UNPKG

20.4 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3*/
4
5"use strict";
6
7const createHash = require("../util/createHash");
8const ArraySerializer = require("./ArraySerializer");
9const DateObjectSerializer = require("./DateObjectSerializer");
10const ErrorObjectSerializer = require("./ErrorObjectSerializer");
11const MapObjectSerializer = require("./MapObjectSerializer");
12const NullPrototypeObjectSerializer = require("./NullPrototypeObjectSerializer");
13const PlainObjectSerializer = require("./PlainObjectSerializer");
14const RegExpObjectSerializer = require("./RegExpObjectSerializer");
15const SerializerMiddleware = require("./SerializerMiddleware");
16const SetObjectSerializer = require("./SetObjectSerializer");
17
18/** @typedef {typeof import("../util/Hash")} Hash */
19/** @typedef {import("./types").ComplexSerializableType} ComplexSerializableType */
20/** @typedef {import("./types").PrimitiveSerializableType} PrimitiveSerializableType */
21
22/** @typedef {new (...params: any[]) => any} Constructor */
23
24/*
25
26Format:
27
28File -> Section*
29Section -> ObjectSection | ReferenceSection | EscapeSection | OtherSection
30
31ObjectSection -> ESCAPE (
32 number:relativeOffset (number > 0) |
33 string:request (string|null):export
34) Section:value* ESCAPE ESCAPE_END_OBJECT
35ReferenceSection -> ESCAPE number:relativeOffset (number < 0)
36EscapeSection -> ESCAPE ESCAPE_ESCAPE_VALUE (escaped value ESCAPE)
37EscapeSection -> ESCAPE ESCAPE_UNDEFINED (escaped value ESCAPE)
38OtherSection -> any (except ESCAPE)
39
40Why using null as escape value?
41Multiple null values can merged by the BinaryMiddleware, which makes it very efficient
42Technically any value can be used.
43
44*/
45
46/**
47 * @typedef {Object} ObjectSerializerContext
48 * @property {function(any): void} write
49 */
50
51/**
52 * @typedef {Object} ObjectDeserializerContext
53 * @property {function(): any} read
54 */
55
56/**
57 * @typedef {Object} ObjectSerializer
58 * @property {function(any, ObjectSerializerContext): void} serialize
59 * @property {function(ObjectDeserializerContext): any} deserialize
60 */
61
62const setSetSize = (set, size) => {
63 let i = 0;
64 for (const item of set) {
65 if (i++ >= size) {
66 set.delete(item);
67 }
68 }
69};
70
71const setMapSize = (map, size) => {
72 let i = 0;
73 for (const item of map.keys()) {
74 if (i++ >= size) {
75 map.delete(item);
76 }
77 }
78};
79
80/**
81 * @param {Buffer} buffer buffer
82 * @param {string | Hash} hashFunction hash function to use
83 * @returns {string} hash
84 */
85const toHash = (buffer, hashFunction) => {
86 const hash = createHash(hashFunction);
87 hash.update(buffer);
88 return /** @type {string} */ (hash.digest("latin1"));
89};
90
91const ESCAPE = null;
92const ESCAPE_ESCAPE_VALUE = null;
93const ESCAPE_END_OBJECT = true;
94const ESCAPE_UNDEFINED = false;
95
96const CURRENT_VERSION = 2;
97
98const serializers = new Map();
99const serializerInversed = new Map();
100
101const loadedRequests = new Set();
102
103const NOT_SERIALIZABLE = {};
104
105const jsTypes = new Map();
106jsTypes.set(Object, new PlainObjectSerializer());
107jsTypes.set(Array, new ArraySerializer());
108jsTypes.set(null, new NullPrototypeObjectSerializer());
109jsTypes.set(Map, new MapObjectSerializer());
110jsTypes.set(Set, new SetObjectSerializer());
111jsTypes.set(Date, new DateObjectSerializer());
112jsTypes.set(RegExp, new RegExpObjectSerializer());
113jsTypes.set(Error, new ErrorObjectSerializer(Error));
114jsTypes.set(EvalError, new ErrorObjectSerializer(EvalError));
115jsTypes.set(RangeError, new ErrorObjectSerializer(RangeError));
116jsTypes.set(ReferenceError, new ErrorObjectSerializer(ReferenceError));
117jsTypes.set(SyntaxError, new ErrorObjectSerializer(SyntaxError));
118jsTypes.set(TypeError, new ErrorObjectSerializer(TypeError));
119
120// If in a sandboxed environment (e. g. jest), this escapes the sandbox and registers
121// real Object and Array types to. These types may occur in the wild too, e. g. when
122// using Structured Clone in postMessage.
123if (exports.constructor !== Object) {
124 const Obj = /** @type {typeof Object} */ (exports.constructor);
125 const Fn = /** @type {typeof Function} */ (Obj.constructor);
126 for (const [type, config] of Array.from(jsTypes)) {
127 if (type) {
128 const Type = new Fn(`return ${type.name};`)();
129 jsTypes.set(Type, config);
130 }
131 }
132}
133
134{
135 let i = 1;
136 for (const [type, serializer] of jsTypes) {
137 serializers.set(type, {
138 request: "",
139 name: i++,
140 serializer
141 });
142 }
143}
144
145for (const { request, name, serializer } of serializers.values()) {
146 serializerInversed.set(`${request}/${name}`, serializer);
147}
148
149/** @type {Map<RegExp, (request: string) => boolean>} */
150const loaders = new Map();
151
152/**
153 * @typedef {ComplexSerializableType[]} DeserializedType
154 * @typedef {PrimitiveSerializableType[]} SerializedType
155 * @extends {SerializerMiddleware<DeserializedType, SerializedType>}
156 */
157class ObjectMiddleware extends SerializerMiddleware {
158 /**
159 * @param {function(any): void} extendContext context extensions
160 * @param {string | Hash} hashFunction hash function to use
161 */
162 constructor(extendContext, hashFunction = "md4") {
163 super();
164 this.extendContext = extendContext;
165 this._hashFunction = hashFunction;
166 }
167 /**
168 * @param {RegExp} regExp RegExp for which the request is tested
169 * @param {function(string): boolean} loader loader to load the request, returns true when successful
170 * @returns {void}
171 */
172 static registerLoader(regExp, loader) {
173 loaders.set(regExp, loader);
174 }
175
176 /**
177 * @param {Constructor} Constructor the constructor
178 * @param {string} request the request which will be required when deserializing
179 * @param {string} name the name to make multiple serializer unique when sharing a request
180 * @param {ObjectSerializer} serializer the serializer
181 * @returns {void}
182 */
183 static register(Constructor, request, name, serializer) {
184 const key = request + "/" + name;
185
186 if (serializers.has(Constructor)) {
187 throw new Error(
188 `ObjectMiddleware.register: serializer for ${Constructor.name} is already registered`
189 );
190 }
191
192 if (serializerInversed.has(key)) {
193 throw new Error(
194 `ObjectMiddleware.register: serializer for ${key} is already registered`
195 );
196 }
197
198 serializers.set(Constructor, {
199 request,
200 name,
201 serializer
202 });
203
204 serializerInversed.set(key, serializer);
205 }
206
207 /**
208 * @param {Constructor} Constructor the constructor
209 * @returns {void}
210 */
211 static registerNotSerializable(Constructor) {
212 if (serializers.has(Constructor)) {
213 throw new Error(
214 `ObjectMiddleware.registerNotSerializable: serializer for ${Constructor.name} is already registered`
215 );
216 }
217
218 serializers.set(Constructor, NOT_SERIALIZABLE);
219 }
220
221 static getSerializerFor(object) {
222 const proto = Object.getPrototypeOf(object);
223 let c;
224 if (proto === null) {
225 // Object created with Object.create(null)
226 c = null;
227 } else {
228 c = proto.constructor;
229 if (!c) {
230 throw new Error(
231 "Serialization of objects with prototype without valid constructor property not possible"
232 );
233 }
234 }
235 const config = serializers.get(c);
236
237 if (!config) throw new Error(`No serializer registered for ${c.name}`);
238 if (config === NOT_SERIALIZABLE) throw NOT_SERIALIZABLE;
239
240 return config;
241 }
242
243 static getDeserializerFor(request, name) {
244 const key = request + "/" + name;
245 const serializer = serializerInversed.get(key);
246
247 if (serializer === undefined) {
248 throw new Error(`No deserializer registered for ${key}`);
249 }
250
251 return serializer;
252 }
253
254 static _getDeserializerForWithoutError(request, name) {
255 const key = request + "/" + name;
256 const serializer = serializerInversed.get(key);
257 return serializer;
258 }
259
260 /**
261 * @param {DeserializedType} data data
262 * @param {Object} context context object
263 * @returns {SerializedType|Promise<SerializedType>} serialized data
264 */
265 serialize(data, context) {
266 /** @type {any[]} */
267 let result = [CURRENT_VERSION];
268 let currentPos = 0;
269 let referenceable = new Map();
270 const addReferenceable = item => {
271 referenceable.set(item, currentPos++);
272 };
273 let bufferDedupeMap = new Map();
274 const dedupeBuffer = buf => {
275 const len = buf.length;
276 const entry = bufferDedupeMap.get(len);
277 if (entry === undefined) {
278 bufferDedupeMap.set(len, buf);
279 return buf;
280 }
281 if (Buffer.isBuffer(entry)) {
282 if (len < 32) {
283 if (buf.equals(entry)) {
284 return entry;
285 }
286 bufferDedupeMap.set(len, [entry, buf]);
287 return buf;
288 } else {
289 const hash = toHash(entry, this._hashFunction);
290 const newMap = new Map();
291 newMap.set(hash, entry);
292 bufferDedupeMap.set(len, newMap);
293 const hashBuf = toHash(buf, this._hashFunction);
294 if (hash === hashBuf) {
295 return entry;
296 }
297 return buf;
298 }
299 } else if (Array.isArray(entry)) {
300 if (entry.length < 16) {
301 for (const item of entry) {
302 if (buf.equals(item)) {
303 return item;
304 }
305 }
306 entry.push(buf);
307 return buf;
308 } else {
309 const newMap = new Map();
310 const hash = toHash(buf, this._hashFunction);
311 let found;
312 for (const item of entry) {
313 const itemHash = toHash(item, this._hashFunction);
314 newMap.set(itemHash, item);
315 if (found === undefined && itemHash === hash) found = item;
316 }
317 bufferDedupeMap.set(len, newMap);
318 if (found === undefined) {
319 newMap.set(hash, buf);
320 return buf;
321 } else {
322 return found;
323 }
324 }
325 } else {
326 const hash = toHash(buf, this._hashFunction);
327 const item = entry.get(hash);
328 if (item !== undefined) {
329 return item;
330 }
331 entry.set(hash, buf);
332 return buf;
333 }
334 };
335 let currentPosTypeLookup = 0;
336 let objectTypeLookup = new Map();
337 const cycleStack = new Set();
338 const stackToString = item => {
339 const arr = Array.from(cycleStack);
340 arr.push(item);
341 return arr
342 .map(item => {
343 if (typeof item === "string") {
344 if (item.length > 100) {
345 return `String ${JSON.stringify(item.slice(0, 100)).slice(
346 0,
347 -1
348 )}..."`;
349 }
350 return `String ${JSON.stringify(item)}`;
351 }
352 try {
353 const { request, name } = ObjectMiddleware.getSerializerFor(item);
354 if (request) {
355 return `${request}${name ? `.${name}` : ""}`;
356 }
357 } catch (e) {
358 // ignore -> fallback
359 }
360 if (typeof item === "object" && item !== null) {
361 if (item.constructor) {
362 if (item.constructor === Object)
363 return `Object { ${Object.keys(item).join(", ")} }`;
364 if (item.constructor === Map) return `Map { ${item.size} items }`;
365 if (item.constructor === Array)
366 return `Array { ${item.length} items }`;
367 if (item.constructor === Set) return `Set { ${item.size} items }`;
368 if (item.constructor === RegExp) return item.toString();
369 return `${item.constructor.name}`;
370 }
371 return `Object [null prototype] { ${Object.keys(item).join(
372 ", "
373 )} }`;
374 }
375 try {
376 return `${item}`;
377 } catch (e) {
378 return `(${e.message})`;
379 }
380 })
381 .join(" -> ");
382 };
383 let hasDebugInfoAttached;
384 let ctx = {
385 write(value, key) {
386 try {
387 process(value);
388 } catch (e) {
389 if (e !== NOT_SERIALIZABLE) {
390 if (hasDebugInfoAttached === undefined)
391 hasDebugInfoAttached = new WeakSet();
392 if (!hasDebugInfoAttached.has(e)) {
393 e.message += `\nwhile serializing ${stackToString(value)}`;
394 hasDebugInfoAttached.add(e);
395 }
396 }
397 throw e;
398 }
399 },
400 setCircularReference(ref) {
401 addReferenceable(ref);
402 },
403 snapshot() {
404 return {
405 length: result.length,
406 cycleStackSize: cycleStack.size,
407 referenceableSize: referenceable.size,
408 currentPos,
409 objectTypeLookupSize: objectTypeLookup.size,
410 currentPosTypeLookup
411 };
412 },
413 rollback(snapshot) {
414 result.length = snapshot.length;
415 setSetSize(cycleStack, snapshot.cycleStackSize);
416 setMapSize(referenceable, snapshot.referenceableSize);
417 currentPos = snapshot.currentPos;
418 setMapSize(objectTypeLookup, snapshot.objectTypeLookupSize);
419 currentPosTypeLookup = snapshot.currentPosTypeLookup;
420 },
421 ...context
422 };
423 this.extendContext(ctx);
424 const process = item => {
425 if (Buffer.isBuffer(item)) {
426 // check if we can emit a reference
427 const ref = referenceable.get(item);
428 if (ref !== undefined) {
429 result.push(ESCAPE, ref - currentPos);
430 return;
431 }
432 const alreadyUsedBuffer = dedupeBuffer(item);
433 if (alreadyUsedBuffer !== item) {
434 const ref = referenceable.get(alreadyUsedBuffer);
435 if (ref !== undefined) {
436 referenceable.set(item, ref);
437 result.push(ESCAPE, ref - currentPos);
438 return;
439 }
440 item = alreadyUsedBuffer;
441 }
442 addReferenceable(item);
443
444 result.push(item);
445 } else if (item === ESCAPE) {
446 result.push(ESCAPE, ESCAPE_ESCAPE_VALUE);
447 } else if (
448 typeof item === "object"
449 // We don't have to check for null as ESCAPE is null and this has been checked before
450 ) {
451 // check if we can emit a reference
452 const ref = referenceable.get(item);
453 if (ref !== undefined) {
454 result.push(ESCAPE, ref - currentPos);
455 return;
456 }
457
458 if (cycleStack.has(item)) {
459 throw new Error(
460 `This is a circular references. To serialize circular references use 'setCircularReference' somewhere in the circle during serialize and deserialize.`
461 );
462 }
463
464 const { request, name, serializer } =
465 ObjectMiddleware.getSerializerFor(item);
466 const key = `${request}/${name}`;
467 const lastIndex = objectTypeLookup.get(key);
468
469 if (lastIndex === undefined) {
470 objectTypeLookup.set(key, currentPosTypeLookup++);
471
472 result.push(ESCAPE, request, name);
473 } else {
474 result.push(ESCAPE, currentPosTypeLookup - lastIndex);
475 }
476
477 cycleStack.add(item);
478
479 try {
480 serializer.serialize(item, ctx);
481 } finally {
482 cycleStack.delete(item);
483 }
484
485 result.push(ESCAPE, ESCAPE_END_OBJECT);
486
487 addReferenceable(item);
488 } else if (typeof item === "string") {
489 if (item.length > 1) {
490 // short strings are shorter when not emitting a reference (this saves 1 byte per empty string)
491 // check if we can emit a reference
492 const ref = referenceable.get(item);
493 if (ref !== undefined) {
494 result.push(ESCAPE, ref - currentPos);
495 return;
496 }
497 addReferenceable(item);
498 }
499
500 if (item.length > 102400 && context.logger) {
501 context.logger.warn(
502 `Serializing big strings (${Math.round(
503 item.length / 1024
504 )}kiB) impacts deserialization performance (consider using Buffer instead and decode when needed)`
505 );
506 }
507
508 result.push(item);
509 } else if (typeof item === "function") {
510 if (!SerializerMiddleware.isLazy(item))
511 throw new Error("Unexpected function " + item);
512 /** @type {SerializedType} */
513 const serializedData =
514 SerializerMiddleware.getLazySerializedValue(item);
515 if (serializedData !== undefined) {
516 if (typeof serializedData === "function") {
517 result.push(serializedData);
518 } else {
519 throw new Error("Not implemented");
520 }
521 } else if (SerializerMiddleware.isLazy(item, this)) {
522 throw new Error("Not implemented");
523 } else {
524 const data = SerializerMiddleware.serializeLazy(item, data =>
525 this.serialize([data], context)
526 );
527 SerializerMiddleware.setLazySerializedValue(item, data);
528 result.push(data);
529 }
530 } else if (item === undefined) {
531 result.push(ESCAPE, ESCAPE_UNDEFINED);
532 } else {
533 result.push(item);
534 }
535 };
536
537 try {
538 for (const item of data) {
539 process(item);
540 }
541 return result;
542 } catch (e) {
543 if (e === NOT_SERIALIZABLE) return null;
544
545 throw e;
546 } finally {
547 // Get rid of these references to avoid leaking memory
548 // This happens because the optimized code v8 generates
549 // is optimized for our "ctx.write" method so it will reference
550 // it from e. g. Dependency.prototype.serialize -(IC)-> ctx.write
551 data =
552 result =
553 referenceable =
554 bufferDedupeMap =
555 objectTypeLookup =
556 ctx =
557 undefined;
558 }
559 }
560
561 /**
562 * @param {SerializedType} data data
563 * @param {Object} context context object
564 * @returns {DeserializedType|Promise<DeserializedType>} deserialized data
565 */
566 deserialize(data, context) {
567 let currentDataPos = 0;
568 const read = () => {
569 if (currentDataPos >= data.length)
570 throw new Error("Unexpected end of stream");
571
572 return data[currentDataPos++];
573 };
574
575 if (read() !== CURRENT_VERSION)
576 throw new Error("Version mismatch, serializer changed");
577
578 let currentPos = 0;
579 let referenceable = [];
580 const addReferenceable = item => {
581 referenceable.push(item);
582 currentPos++;
583 };
584 let currentPosTypeLookup = 0;
585 let objectTypeLookup = [];
586 let result = [];
587 let ctx = {
588 read() {
589 return decodeValue();
590 },
591 setCircularReference(ref) {
592 addReferenceable(ref);
593 },
594 ...context
595 };
596 this.extendContext(ctx);
597 const decodeValue = () => {
598 const item = read();
599
600 if (item === ESCAPE) {
601 const nextItem = read();
602
603 if (nextItem === ESCAPE_ESCAPE_VALUE) {
604 return ESCAPE;
605 } else if (nextItem === ESCAPE_UNDEFINED) {
606 return undefined;
607 } else if (nextItem === ESCAPE_END_OBJECT) {
608 throw new Error(
609 `Unexpected end of object at position ${currentDataPos - 1}`
610 );
611 } else {
612 const request = nextItem;
613 let serializer;
614
615 if (typeof request === "number") {
616 if (request < 0) {
617 // relative reference
618 return referenceable[currentPos + request];
619 }
620 serializer = objectTypeLookup[currentPosTypeLookup - request];
621 } else {
622 if (typeof request !== "string") {
623 throw new Error(
624 `Unexpected type (${typeof request}) of request ` +
625 `at position ${currentDataPos - 1}`
626 );
627 }
628 const name = read();
629
630 serializer = ObjectMiddleware._getDeserializerForWithoutError(
631 request,
632 name
633 );
634
635 if (serializer === undefined) {
636 if (request && !loadedRequests.has(request)) {
637 let loaded = false;
638 for (const [regExp, loader] of loaders) {
639 if (regExp.test(request)) {
640 if (loader(request)) {
641 loaded = true;
642 break;
643 }
644 }
645 }
646 if (!loaded) {
647 require(request);
648 }
649
650 loadedRequests.add(request);
651 }
652
653 serializer = ObjectMiddleware.getDeserializerFor(request, name);
654 }
655
656 objectTypeLookup.push(serializer);
657 currentPosTypeLookup++;
658 }
659 try {
660 const item = serializer.deserialize(ctx);
661 const end1 = read();
662
663 if (end1 !== ESCAPE) {
664 throw new Error("Expected end of object");
665 }
666
667 const end2 = read();
668
669 if (end2 !== ESCAPE_END_OBJECT) {
670 throw new Error("Expected end of object");
671 }
672
673 addReferenceable(item);
674
675 return item;
676 } catch (err) {
677 // As this is only for error handling, we omit creating a Map for
678 // faster access to this information, as this would affect performance
679 // in the good case
680 let serializerEntry;
681 for (const entry of serializers) {
682 if (entry[1].serializer === serializer) {
683 serializerEntry = entry;
684 break;
685 }
686 }
687 const name = !serializerEntry
688 ? "unknown"
689 : !serializerEntry[1].request
690 ? serializerEntry[0].name
691 : serializerEntry[1].name
692 ? `${serializerEntry[1].request} ${serializerEntry[1].name}`
693 : serializerEntry[1].request;
694 err.message += `\n(during deserialization of ${name})`;
695 throw err;
696 }
697 }
698 } else if (typeof item === "string") {
699 if (item.length > 1) {
700 addReferenceable(item);
701 }
702
703 return item;
704 } else if (Buffer.isBuffer(item)) {
705 addReferenceable(item);
706
707 return item;
708 } else if (typeof item === "function") {
709 return SerializerMiddleware.deserializeLazy(
710 item,
711 data => this.deserialize(data, context)[0]
712 );
713 } else {
714 return item;
715 }
716 };
717
718 try {
719 while (currentDataPos < data.length) {
720 result.push(decodeValue());
721 }
722 return result;
723 } finally {
724 // Get rid of these references to avoid leaking memory
725 // This happens because the optimized code v8 generates
726 // is optimized for our "ctx.read" method so it will reference
727 // it from e. g. Dependency.prototype.deserialize -(IC)-> ctx.read
728 result = referenceable = data = objectTypeLookup = ctx = undefined;
729 }
730 }
731}
732
733module.exports = ObjectMiddleware;
734module.exports.NOT_SERIALIZABLE = NOT_SERIALIZABLE;
735
\No newline at end of file