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