import { ECMAArray, IDynamicPropertyOutput } from '../index'; import { Stream } from './stream'; import { Mapping } from '../utils/mapping'; import { Reference } from '../utils/reference'; import { Markers } from '../enums/markers'; import Utils from '../utils/index'; /** * @exports * @class * @implements IDynamicPropertyOutput */ export class Serializer implements IDynamicPropertyOutput { /** * @private * @description The stream holder * @type {Stream} */ private stream: Stream; /** * @private * @description The mapping holder * @type {Mapping} */ private mapping: Mapping; /** * @private * @description The reference holder * @type {Reference} */ private reference: Reference; /** * @constructor * @param {object} options */ constructor(options: { [key: string]: Stream | Mapping; }) { /** * @description Initialize the stream holder * @type {Stream} */ this.stream = options.stream as Stream; /** * @description Initialize the mapping holder * @type {Mapping} */ this.mapping = options.mapping as Mapping; /** * @description Initialize the reference holder * @type {Reference} */ this.reference = new Reference(); } /** * @public * @description Serializes data to AMF0 bytes * @param {any} data * @returns {Array} */ public serialize(data: any): number[] { if (data === null) { this.serializeNull(); } else if (data === undefined) { this.serializeUndefined(); } else { const type: Function = data.constructor; switch (type) { case Boolean: this.serializeBoolean(data); break; case Number: this.serializeNumber(data); break; case String: this.serializeString(data); break; case Object: this.serializeObject(data); break; case Array: this.serializeECMAArray(data); break; case Function: this.serializeUndefined(); break; case Date: this.serializeDate(data); break; case Map: this.serializeMap(data); break; case Set: this.serializeSet(data); break; default: this.serializeUnidentifiedObject(data); } } return this.stream.data; } /** * @public * @description Writes the name and value of an IDynamicPropertyOutput object to an object with dynamic properties * @param {string} name * @param {any} value * @returns {void} */ public writeDynamicProperty(name: string, value: any): void { this.stream.writeUTF(name); this.serialize(value); } /** * @private * @description Serializes a null * @returns {void} */ private serializeNull(): void { this.stream.writeUnsignedByte(Markers.NULL); } /** * @private * @description Serializes an undefined * @returns {void} */ private serializeUndefined(): void { this.stream.writeUnsignedByte(Markers.UNDEFINED); } /** * @private * @description Serializes a boolean * @param {boolean|Boolean} value * @returns {void} */ private serializeBoolean(value: boolean | Boolean): void { // Primitive Boolean can't be compared if (value instanceof Boolean) { value = value.valueOf(); } this.stream.writeUnsignedByte(Markers.BOOLEAN); this.stream.writeBoolean(value as boolean); } /** * @private * @description Serializes a number * @param {number} value * @returns {void} */ private serializeNumber(value: number): void { this.stream.writeUnsignedByte(Markers.NUMBER); this.stream.writeDouble(value); } /** * @private * @description Serializes a (long) string * @param {string} value * @returns {void} */ private serializeString(value: string): void { const length: number = Utils.byteLength(value); const isLong: boolean = length > 65535; this.stream.writeUnsignedByte(isLong ? Markers.LONG_STRING : Markers.STRING); isLong ? this.stream.writeUnsignedInt(length) : this.stream.writeUnsignedShort(length); this.stream.writeUTFBytes(value); } /** * @private * @description Serializes a reference * @param {number} idx * @returns {void} */ private serializeReference(idx: number): void { this.stream.writeUnsignedByte(Markers.REFERENCE); this.stream.writeUnsignedShort(idx); } /** * @private * @description Serializes an object end * @returns {void} */ private serializeObjectEnd(): void { this.stream.writeShort(0); // UTF-8-empty this.stream.writeUnsignedByte(Markers.OBJECT_END); } /** * @private * @description Serializes an object * @param {object} value * @returns {void} */ private serializeObject(value: { [key: string]: any; }): void { const idx: number | boolean = this.reference.check(value); if (idx !== false) { return this.serializeReference(idx as number); } this.stream.writeUnsignedByte(Markers.OBJECT); if (this.mapping.hasDynamicPropertyWriter()) { const dpw: object = this.mapping.getDynamicPropertyWriter() as object; if (Utils.isDynamicPropertyWriterClass(dpw)) { dpw.writeDynamicProperties(value, this); } } else { for (const key in value) { this.stream.writeUTF(key); this.serialize(value[key]); } } this.serializeObjectEnd(); } /** * @private * @description Serializes an ECMA array * @param {ECMAArray} value * @returns {void} */ private serializeECMAArray(value: ECMAArray): void { const idx: number | boolean = this.reference.check(value); if (idx !== false) { return this.serializeReference(idx as number); } this.stream.writeUnsignedByte(Markers.ECMA_ARRAY); this.stream.writeUnsignedInt(Object.keys(value).length); for (const element in value) { this.stream.writeUTF(String(element)); this.serialize(value[element]); } this.serializeObjectEnd(); } /** * @private * @description Serializes a date * @param {Date} value * @returns {void} */ private serializeDate(value: Date): void { const idx: number | boolean = this.reference.check(value); if (idx !== false) { return this.serializeReference(idx as number); } this.stream.writeUnsignedByte(Markers.DATE); this.stream.writeDouble(value.getTime()); this.stream.writeShort(value.getTimezoneOffset()); // Reserved timezone offset, which isn't 0x0000 } /** * @private * @description Serializes a map * @param {Map} value * @returns {void} */ private serializeMap(value: Map): void { const idx: number | boolean = this.reference.check(value); if (idx !== false) { return this.serializeReference(idx as number); } this.stream.writeUnsignedByte(Markers.MAP); for (const [key, data] of value) { this.stream.writeUTF(key); this.serialize(data); } this.serializeObjectEnd(); } /** * @private * @description Serializes a set * @param {Set} value * @returns {void} */ private serializeSet(value: Set): void { const idx: number | boolean = this.reference.check(value); if (idx !== false) { return this.serializeReference(idx as number); } this.stream.writeUnsignedByte(Markers.SET); this.stream.writeUnsignedInt(value.size); for (const element of value) { this.serialize(element); } this.serializeObjectEnd(); } /** * @private * @description Serializes an unidentified object * @param {object} value * @returns {void} */ private serializeUnidentifiedObject(value: { [key: string]: any; }): void { const type: Function = Object.getPrototypeOf(value).constructor; if (this.mapping.isRegisteredClassAlias(type)) { this.serializeTypedObject(value); } else { Utils.isClass(type.name.toString()) ? this.serializeObject(value) : this.serializeUnsupported(); } } /** * @private * @description Serializes a typed object * @param {object} value * @returns {void} */ private serializeTypedObject(value: { [key: string]: any; }): void { const idx: number | boolean = this.reference.check(value); if (idx !== false) { return this.serializeReference(idx as number); } this.stream.writeUnsignedByte(Markers.TYPED_OBJECT); this.stream.writeUTF(this.mapping.getQualifiedClassName(value.constructor)); for (const key in value) { this.stream.writeUTF(key); this.serialize(value[key]); } this.serializeObjectEnd(); } /** * @private * @description Serializes an unsupported value * @returns {void} */ private serializeUnsupported(): void { this.stream.writeUnsignedByte(Markers.UNSUPPORTED); } }