| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845 | /*	MIT License http://www.opensource.org/licenses/mit-license.php*/"use strict";const { DEFAULTS } = require("../config/defaults");const createHash = require("../util/createHash");const AggregateErrorSerializer = require("./AggregateErrorSerializer");const ArraySerializer = require("./ArraySerializer");const DateObjectSerializer = require("./DateObjectSerializer");const ErrorObjectSerializer = require("./ErrorObjectSerializer");const MapObjectSerializer = require("./MapObjectSerializer");const NullPrototypeObjectSerializer = require("./NullPrototypeObjectSerializer");const PlainObjectSerializer = require("./PlainObjectSerializer");const RegExpObjectSerializer = require("./RegExpObjectSerializer");const SerializerMiddleware = require("./SerializerMiddleware");const SetObjectSerializer = require("./SetObjectSerializer");/** @typedef {import("../logging/Logger").Logger} Logger *//** @typedef {typeof import("../util/Hash")} Hash *//** @typedef {import("./SerializerMiddleware").LazyOptions} LazyOptions *//** @typedef {import("./types").ComplexSerializableType} ComplexSerializableType *//** @typedef {import("./types").PrimitiveSerializableType} PrimitiveSerializableType *//** @typedef {new (...params: EXPECTED_ANY[]) => EXPECTED_ANY} Constructor *//*Format:File -> Section*Section -> ObjectSection | ReferenceSection | EscapeSection | OtherSectionObjectSection -> ESCAPE (	number:relativeOffset (number > 0) |	string:request (string|null):export) Section:value* ESCAPE ESCAPE_END_OBJECTReferenceSection -> ESCAPE number:relativeOffset (number < 0)EscapeSection -> ESCAPE ESCAPE_ESCAPE_VALUE (escaped value ESCAPE)EscapeSection -> ESCAPE ESCAPE_UNDEFINED (escaped value ESCAPE)OtherSection -> any (except ESCAPE)Why using null as escape value?Multiple null values can merged by the BinaryMiddleware, which makes it very efficientTechnically any value can be used.*//** * @typedef {object} ObjectSerializerSnapshot * @property {number} length * @property {number} cycleStackSize * @property {number} referenceableSize * @property {number} currentPos * @property {number} objectTypeLookupSize * @property {number} currentPosTypeLookup *//** @typedef {TODO} Value *//** @typedef {EXPECTED_OBJECT | string} ReferenceableItem *//** * @typedef {object} ObjectSerializerContext * @property {(value: Value) => void} write * @property {(value: ReferenceableItem) => void} setCircularReference * @property {() => ObjectSerializerSnapshot} snapshot * @property {(snapshot: ObjectSerializerSnapshot) => void} rollback * @property {((item: Value | (() => Value)) => void)=} writeLazy * @property {((item: (Value | (() => Value)), obj: LazyOptions | undefined) => import("./SerializerMiddleware").LazyFunction<EXPECTED_ANY, EXPECTED_ANY, EXPECTED_ANY, LazyOptions>)=} writeSeparate *//** * @typedef {object} ObjectDeserializerContext * @property {() => Value} read * @property {(value: ReferenceableItem) => void} setCircularReference *//** * @typedef {object} ObjectSerializer * @property {(value: Value, context: ObjectSerializerContext) => void} serialize * @property {(context: ObjectDeserializerContext) => Value} deserialize *//** * @template T * @param {Set<T>} set set * @param {number} size count of items to keep */const setSetSize = (set, size) => {	let i = 0;	for (const item of set) {		if (i++ >= size) {			set.delete(item);		}	}};/** * @template K, X * @param {Map<K, X>} map map * @param {number} size count of items to keep */const setMapSize = (map, size) => {	let i = 0;	for (const item of map.keys()) {		if (i++ >= size) {			map.delete(item);		}	}};/** * @param {Buffer} buffer buffer * @param {string | Hash} hashFunction hash function to use * @returns {string} hash */const toHash = (buffer, hashFunction) => {	const hash = createHash(hashFunction);	hash.update(buffer);	return /** @type {string} */ (hash.digest("latin1"));};const ESCAPE = null;const ESCAPE_ESCAPE_VALUE = null;const ESCAPE_END_OBJECT = true;const ESCAPE_UNDEFINED = false;const CURRENT_VERSION = 2;/** @typedef {{ request?: string, name?: string | number | null, serializer?: ObjectSerializer }} SerializerConfig *//** @typedef {{ request?: string, name?: string | number | null, serializer: ObjectSerializer }} SerializerConfigWithSerializer *//** @type {Map<Constructor, SerializerConfig>} */const serializers = new Map();/** @type {Map<string | number, ObjectSerializer>} */const serializerInversed = new Map();/** @type {Set<string>} */const loadedRequests = new Set();const NOT_SERIALIZABLE = {};const jsTypes = new Map();jsTypes.set(Object, new PlainObjectSerializer());jsTypes.set(Array, new ArraySerializer());jsTypes.set(null, new NullPrototypeObjectSerializer());jsTypes.set(Map, new MapObjectSerializer());jsTypes.set(Set, new SetObjectSerializer());jsTypes.set(Date, new DateObjectSerializer());jsTypes.set(RegExp, new RegExpObjectSerializer());jsTypes.set(Error, new ErrorObjectSerializer(Error));jsTypes.set(EvalError, new ErrorObjectSerializer(EvalError));jsTypes.set(RangeError, new ErrorObjectSerializer(RangeError));jsTypes.set(ReferenceError, new ErrorObjectSerializer(ReferenceError));jsTypes.set(SyntaxError, new ErrorObjectSerializer(SyntaxError));jsTypes.set(TypeError, new ErrorObjectSerializer(TypeError));// @ts-expect-error ES2018 doesn't `AggregateError`, but it can be used by developers// eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntaxif (typeof AggregateError !== "undefined") {	jsTypes.set(		// @ts-expect-error ES2018 doesn't `AggregateError`, but it can be used by developers		// eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax		AggregateError,		new AggregateErrorSerializer()	);}// If in a sandboxed environment (e.g. jest), this escapes the sandbox and registers// real Object and Array types to. These types may occur in the wild too, e.g. when// using Structured Clone in postMessage.// eslint-disable-next-line n/exports-styleif (exports.constructor !== Object) {	// eslint-disable-next-line n/exports-style	const Obj = /** @type {ObjectConstructor} */ (exports.constructor);	const Fn = /** @type {FunctionConstructor} */ (Obj.constructor);	for (const [type, config] of jsTypes) {		if (type) {			const Type = new Fn(`return ${type.name};`)();			jsTypes.set(Type, config);		}	}}{	let i = 1;	for (const [type, serializer] of jsTypes) {		serializers.set(type, {			request: "",			name: i++,			serializer		});	}}for (const { request, name, serializer } of serializers.values()) {	serializerInversed.set(		`${request}/${name}`,		/** @type {ObjectSerializer} */ (serializer)	);}/** @type {Map<RegExp, (request: string) => boolean>} */const loaders = new Map();/** @typedef {ComplexSerializableType[]} DeserializedType *//** @typedef {PrimitiveSerializableType[]} SerializedType *//** @typedef {{ logger: Logger }} Context *//** * @extends {SerializerMiddleware<DeserializedType, SerializedType, Context>} */class ObjectMiddleware extends SerializerMiddleware {	/**	 * @param {(context: ObjectSerializerContext | ObjectDeserializerContext) => void} extendContext context extensions	 * @param {string | Hash} hashFunction hash function to use	 */	constructor(extendContext, hashFunction = DEFAULTS.HASH_FUNCTION) {		super();		this.extendContext = extendContext;		this._hashFunction = hashFunction;	}	/**	 * @param {RegExp} regExp RegExp for which the request is tested	 * @param {(request: string) => boolean} loader loader to load the request, returns true when successful	 * @returns {void}	 */	static registerLoader(regExp, loader) {		loaders.set(regExp, loader);	}	/**	 * @param {Constructor} Constructor the constructor	 * @param {string} request the request which will be required when deserializing	 * @param {string | null} name the name to make multiple serializer unique when sharing a request	 * @param {ObjectSerializer} serializer the serializer	 * @returns {void}	 */	static register(Constructor, request, name, serializer) {		const key = `${request}/${name}`;		if (serializers.has(Constructor)) {			throw new Error(				`ObjectMiddleware.register: serializer for ${Constructor.name} is already registered`			);		}		if (serializerInversed.has(key)) {			throw new Error(				`ObjectMiddleware.register: serializer for ${key} is already registered`			);		}		serializers.set(Constructor, {			request,			name,			serializer		});		serializerInversed.set(key, serializer);	}	/**	 * @param {Constructor} Constructor the constructor	 * @returns {void}	 */	static registerNotSerializable(Constructor) {		if (serializers.has(Constructor)) {			throw new Error(				`ObjectMiddleware.registerNotSerializable: serializer for ${Constructor.name} is already registered`			);		}		serializers.set(Constructor, NOT_SERIALIZABLE);	}	/**	 * @param {Constructor} object for serialization	 * @returns {SerializerConfigWithSerializer} Serializer config	 */	static getSerializerFor(object) {		const proto = Object.getPrototypeOf(object);		let c;		if (proto === null) {			// Object created with Object.create(null)			c = null;		} else {			c = proto.constructor;			if (!c) {				throw new Error(					"Serialization of objects with prototype without valid constructor property not possible"				);			}		}		const config = serializers.get(c);		if (!config) throw new Error(`No serializer registered for ${c.name}`);		if (config === NOT_SERIALIZABLE) throw NOT_SERIALIZABLE;		return /** @type {SerializerConfigWithSerializer} */ (config);	}	/**	 * @param {string} request request	 * @param {string} name name	 * @returns {ObjectSerializer} serializer	 */	static getDeserializerFor(request, name) {		const key = `${request}/${name}`;		const serializer = serializerInversed.get(key);		if (serializer === undefined) {			throw new Error(`No deserializer registered for ${key}`);		}		return serializer;	}	/**	 * @param {string} request request	 * @param {string} name name	 * @returns {ObjectSerializer | undefined} serializer	 */	static _getDeserializerForWithoutError(request, name) {		const key = `${request}/${name}`;		const serializer = serializerInversed.get(key);		return serializer;	}	/**	 * @param {DeserializedType} data data	 * @param {Context} context context object	 * @returns {SerializedType | Promise<SerializedType> | null} serialized data	 */	serialize(data, context) {		/** @type {Value[]} */		let result = [CURRENT_VERSION];		let currentPos = 0;		/** @type {Map<ReferenceableItem, number>} */		let referenceable = new Map();		/**		 * @param {ReferenceableItem} item referenceable item		 */		const addReferenceable = item => {			referenceable.set(item, currentPos++);		};		let bufferDedupeMap = new Map();		/**		 * @param {Buffer} buf buffer		 * @returns {Buffer} deduped buffer		 */		const dedupeBuffer = buf => {			const len = buf.length;			const entry = bufferDedupeMap.get(len);			if (entry === undefined) {				bufferDedupeMap.set(len, buf);				return buf;			}			if (Buffer.isBuffer(entry)) {				if (len < 32) {					if (buf.equals(entry)) {						return entry;					}					bufferDedupeMap.set(len, [entry, buf]);					return buf;				}				const hash = toHash(entry, this._hashFunction);				const newMap = new Map();				newMap.set(hash, entry);				bufferDedupeMap.set(len, newMap);				const hashBuf = toHash(buf, this._hashFunction);				if (hash === hashBuf) {					return entry;				}				return buf;			} else if (Array.isArray(entry)) {				if (entry.length < 16) {					for (const item of entry) {						if (buf.equals(item)) {							return item;						}					}					entry.push(buf);					return buf;				}				const newMap = new Map();				const hash = toHash(buf, this._hashFunction);				let found;				for (const item of entry) {					const itemHash = toHash(item, this._hashFunction);					newMap.set(itemHash, item);					if (found === undefined && itemHash === hash) found = item;				}				bufferDedupeMap.set(len, newMap);				if (found === undefined) {					newMap.set(hash, buf);					return buf;				}				return found;			}			const hash = toHash(buf, this._hashFunction);			const item = entry.get(hash);			if (item !== undefined) {				return item;			}			entry.set(hash, buf);			return buf;		};		let currentPosTypeLookup = 0;		let objectTypeLookup = new Map();		const cycleStack = new Set();		/**		 * @param {Value} item item to stack		 * @returns {string} stack		 */		const stackToString = item => {			const arr = [...cycleStack];			arr.push(item);			return arr				.map(item => {					if (typeof item === "string") {						if (item.length > 100) {							return `String ${JSON.stringify(item.slice(0, 100)).slice(								0,								-1							)}..."`;						}						return `String ${JSON.stringify(item)}`;					}					try {						const { request, name } = ObjectMiddleware.getSerializerFor(item);						if (request) {							return `${request}${name ? `.${name}` : ""}`;						}					} catch (_err) {						// ignore -> fallback					}					if (typeof item === "object" && item !== null) {						if (item.constructor) {							if (item.constructor === Object) {								return `Object { ${Object.keys(item).join(", ")} }`;							}							if (item.constructor === Map) return `Map { ${item.size} items }`;							if (item.constructor === Array) {								return `Array { ${item.length} items }`;							}							if (item.constructor === Set) return `Set { ${item.size} items }`;							if (item.constructor === RegExp) return item.toString();							return `${item.constructor.name}`;						}						return `Object [null prototype] { ${Object.keys(item).join(							", "						)} }`;					}					if (typeof item === "bigint") {						return `BigInt ${item}n`;					}					try {						return `${item}`;					} catch (err) {						return `(${/** @type {Error} */ (err).message})`;					}				})				.join(" -> ");		};		/** @type {WeakSet<Error>} */		let hasDebugInfoAttached;		/** @type {ObjectSerializerContext} */		let ctx = {			write(value) {				try {					process(value);				} catch (err) {					if (err !== NOT_SERIALIZABLE) {						if (hasDebugInfoAttached === undefined) {							hasDebugInfoAttached = new WeakSet();						}						if (!hasDebugInfoAttached.has(/** @type {Error} */ (err))) {							/** @type {Error} */							(err).message += `\nwhile serializing ${stackToString(value)}`;							hasDebugInfoAttached.add(/** @type {Error} */ (err));						}					}					throw err;				}			},			setCircularReference(ref) {				addReferenceable(ref);			},			snapshot() {				return {					length: result.length,					cycleStackSize: cycleStack.size,					referenceableSize: referenceable.size,					currentPos,					objectTypeLookupSize: objectTypeLookup.size,					currentPosTypeLookup				};			},			rollback(snapshot) {				result.length = snapshot.length;				setSetSize(cycleStack, snapshot.cycleStackSize);				setMapSize(referenceable, snapshot.referenceableSize);				currentPos = snapshot.currentPos;				setMapSize(objectTypeLookup, snapshot.objectTypeLookupSize);				currentPosTypeLookup = snapshot.currentPosTypeLookup;			},			...context		};		this.extendContext(ctx);		/**		 * @param {Value} item item to serialize		 */		const process = item => {			if (Buffer.isBuffer(item)) {				// check if we can emit a reference				const ref = referenceable.get(item);				if (ref !== undefined) {					result.push(ESCAPE, ref - currentPos);					return;				}				const alreadyUsedBuffer = dedupeBuffer(item);				if (alreadyUsedBuffer !== item) {					const ref = referenceable.get(alreadyUsedBuffer);					if (ref !== undefined) {						referenceable.set(item, ref);						result.push(ESCAPE, ref - currentPos);						return;					}					item = alreadyUsedBuffer;				}				addReferenceable(item);				result.push(item);			} else if (item === ESCAPE) {				result.push(ESCAPE, ESCAPE_ESCAPE_VALUE);			} else if (				typeof item === "object"				// We don't have to check for null as ESCAPE is null and this has been checked before			) {				// check if we can emit a reference				const ref = referenceable.get(item);				if (ref !== undefined) {					result.push(ESCAPE, ref - currentPos);					return;				}				if (cycleStack.has(item)) {					throw new Error(						"This is a circular references. To serialize circular references use 'setCircularReference' somewhere in the circle during serialize and deserialize."					);				}				const { request, name, serializer } =					ObjectMiddleware.getSerializerFor(item);				const key = `${request}/${name}`;				const lastIndex = objectTypeLookup.get(key);				if (lastIndex === undefined) {					objectTypeLookup.set(key, currentPosTypeLookup++);					result.push(ESCAPE, request, name);				} else {					result.push(ESCAPE, currentPosTypeLookup - lastIndex);				}				cycleStack.add(item);				try {					serializer.serialize(item, ctx);				} finally {					cycleStack.delete(item);				}				result.push(ESCAPE, ESCAPE_END_OBJECT);				addReferenceable(item);			} else if (typeof item === "string") {				if (item.length > 1) {					// short strings are shorter when not emitting a reference (this saves 1 byte per empty string)					// check if we can emit a reference					const ref = referenceable.get(item);					if (ref !== undefined) {						result.push(ESCAPE, ref - currentPos);						return;					}					addReferenceable(item);				}				if (item.length > 102400 && context.logger) {					context.logger.warn(						`Serializing big strings (${Math.round(							item.length / 1024						)}kiB) impacts deserialization performance (consider using Buffer instead and decode when needed)`					);				}				result.push(item);			} else if (typeof item === "function") {				if (!SerializerMiddleware.isLazy(item)) {					throw new Error(`Unexpected function ${item}`);				}				/** @type {SerializedType | undefined} */				const serializedData =					SerializerMiddleware.getLazySerializedValue(item);				if (serializedData !== undefined) {					if (typeof serializedData === "function") {						result.push(serializedData);					} else {						throw new Error("Not implemented");					}				} else if (SerializerMiddleware.isLazy(item, this)) {					throw new Error("Not implemented");				} else {					const data = SerializerMiddleware.serializeLazy(item, data =>						this.serialize([data], context)					);					SerializerMiddleware.setLazySerializedValue(item, data);					result.push(data);				}			} else if (item === undefined) {				result.push(ESCAPE, ESCAPE_UNDEFINED);			} else {				result.push(item);			}		};		try {			for (const item of data) {				process(item);			}			return result;		} catch (err) {			if (err === NOT_SERIALIZABLE) return null;			throw err;		} finally {			// Get rid of these references to avoid leaking memory			// This happens because the optimized code v8 generates			// is optimized for our "ctx.write" method so it will reference			// it from e. g. Dependency.prototype.serialize -(IC)-> ctx.write			data =				result =				referenceable =				bufferDedupeMap =				objectTypeLookup =				ctx =					/** @type {EXPECTED_ANY} */					(undefined);		}	}	/**	 * @param {SerializedType} data data	 * @param {Context} context context object	 * @returns {DeserializedType | Promise<DeserializedType>} deserialized data	 */	deserialize(data, context) {		let currentDataPos = 0;		const read = () => {			if (currentDataPos >= data.length) {				throw new Error("Unexpected end of stream");			}			return data[currentDataPos++];		};		if (read() !== CURRENT_VERSION) {			throw new Error("Version mismatch, serializer changed");		}		let currentPos = 0;		/** @type {ReferenceableItem[]} */		let referenceable = [];		/**		 * @param {Value} item referenceable item		 */		const addReferenceable = item => {			referenceable.push(item);			currentPos++;		};		let currentPosTypeLookup = 0;		/** @type {ObjectSerializer[]} */		let objectTypeLookup = [];		let result = [];		/** @type {ObjectDeserializerContext} */		let ctx = {			read() {				return decodeValue();			},			setCircularReference(ref) {				addReferenceable(ref);			},			...context		};		this.extendContext(ctx);		const decodeValue = () => {			const item = read();			if (item === ESCAPE) {				const nextItem = read();				if (nextItem === ESCAPE_ESCAPE_VALUE) {					return ESCAPE;				} else if (nextItem === ESCAPE_UNDEFINED) {					// Nothing				} else if (nextItem === ESCAPE_END_OBJECT) {					throw new Error(						`Unexpected end of object at position ${currentDataPos - 1}`					);				} else {					const request = nextItem;					let serializer;					if (typeof request === "number") {						if (request < 0) {							// relative reference							return referenceable[currentPos + request];						}						serializer = objectTypeLookup[currentPosTypeLookup - request];					} else {						if (typeof request !== "string") {							throw new Error(								`Unexpected type (${typeof request}) of request ` +									`at position ${currentDataPos - 1}`							);						}						const name = /** @type {string} */ (read());						serializer = ObjectMiddleware._getDeserializerForWithoutError(							request,							name						);						if (serializer === undefined) {							if (request && !loadedRequests.has(request)) {								let loaded = false;								for (const [regExp, loader] of loaders) {									if (regExp.test(request) && loader(request)) {										loaded = true;										break;									}								}								if (!loaded) {									require(request);								}								loadedRequests.add(request);							}							serializer = ObjectMiddleware.getDeserializerFor(request, name);						}						objectTypeLookup.push(serializer);						currentPosTypeLookup++;					}					try {						const item = serializer.deserialize(ctx);						const end1 = read();						if (end1 !== ESCAPE) {							throw new Error("Expected end of object");						}						const end2 = read();						if (end2 !== ESCAPE_END_OBJECT) {							throw new Error("Expected end of object");						}						addReferenceable(item);						return item;					} catch (err) {						// As this is only for error handling, we omit creating a Map for						// faster access to this information, as this would affect performance						// in the good case						let serializerEntry;						for (const entry of serializers) {							if (entry[1].serializer === serializer) {								serializerEntry = entry;								break;							}						}						const name = !serializerEntry							? "unknown"							: !serializerEntry[1].request								? serializerEntry[0].name								: serializerEntry[1].name									? `${serializerEntry[1].request} ${serializerEntry[1].name}`									: serializerEntry[1].request;						/** @type {Error} */						(err).message += `\n(during deserialization of ${name})`;						throw err;					}				}			} else if (typeof item === "string") {				if (item.length > 1) {					addReferenceable(item);				}				return item;			} else if (Buffer.isBuffer(item)) {				addReferenceable(item);				return item;			} else if (typeof item === "function") {				return SerializerMiddleware.deserializeLazy(					item,					data =>						/** @type {[DeserializedType]} */						(this.deserialize(data, context))[0]				);			} else {				return item;			}		};		try {			while (currentDataPos < data.length) {				result.push(decodeValue());			}			return result;		} finally {			// Get rid of these references to avoid leaking memory			// This happens because the optimized code v8 generates			// is optimized for our "ctx.read" method so it will reference			// it from e. g. Dependency.prototype.deserialize -(IC)-> ctx.read			result =				referenceable =				data =				objectTypeLookup =				ctx =					/** @type {EXPECTED_ANY} */					(undefined);		}	}}module.exports = ObjectMiddleware;module.exports.NOT_SERIALIZABLE = NOT_SERIALIZABLE;
 |