diff --git a/.gitignore b/.gitignore index 44d646d..3c3629e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ node_modules -dist/ diff --git a/dist/binary.d.ts b/dist/binary.d.ts new file mode 100644 index 0000000..835bd62 --- /dev/null +++ b/dist/binary.d.ts @@ -0,0 +1,20 @@ +/** + * Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder. + * + * @param {Object} packet - socket.io event packet + * @return {Object} with deconstructed packet and list of buffers + * @public + */ +export declare function deconstructPacket(packet: any): { + packet: any; + buffers: any[]; +}; +/** + * Reconstructs a binary packet from its placeholder packet and buffers + * + * @param {Object} packet - event packet with placeholders + * @param {Array} buffers - binary buffers to put in placeholder positions + * @return {Object} reconstructed packet + * @public + */ +export declare function reconstructPacket(packet: any, buffers: any): any; diff --git a/dist/binary.js b/dist/binary.js new file mode 100644 index 0000000..cbee976 --- /dev/null +++ b/dist/binary.js @@ -0,0 +1,83 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.reconstructPacket = exports.deconstructPacket = void 0; +const is_binary_1 = __importDefault(require("./is-binary")); +/** + * Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder. + * + * @param {Object} packet - socket.io event packet + * @return {Object} with deconstructed packet and list of buffers + * @public + */ +function deconstructPacket(packet) { + const buffers = []; + const packetData = packet.data; + const pack = packet; + pack.data = _deconstructPacket(packetData, buffers); + pack.attachments = buffers.length; // number of binary 'attachments' + return { packet: pack, buffers: buffers }; +} +exports.deconstructPacket = deconstructPacket; +function _deconstructPacket(data, buffers) { + if (!data) + return data; + if (is_binary_1.default(data)) { + const placeholder = { _placeholder: true, num: buffers.length }; + buffers.push(data); + return placeholder; + } + else if (Array.isArray(data)) { + const newData = new Array(data.length); + for (let i = 0; i < data.length; i++) { + newData[i] = _deconstructPacket(data[i], buffers); + } + return newData; + } + else if (typeof data === "object" && !(data instanceof Date)) { + const newData = {}; + for (const key in data) { + if (data.hasOwnProperty(key)) { + newData[key] = _deconstructPacket(data[key], buffers); + } + } + return newData; + } + return data; +} +/** + * Reconstructs a binary packet from its placeholder packet and buffers + * + * @param {Object} packet - event packet with placeholders + * @param {Array} buffers - binary buffers to put in placeholder positions + * @return {Object} reconstructed packet + * @public + */ +function reconstructPacket(packet, buffers) { + packet.data = _reconstructPacket(packet.data, buffers); + packet.attachments = undefined; // no longer useful + return packet; +} +exports.reconstructPacket = reconstructPacket; +function _reconstructPacket(data, buffers) { + if (!data) + return data; + if (data && data._placeholder) { + return buffers[data.num]; // appropriate buffer (should be natural order anyway) + } + else if (Array.isArray(data)) { + for (let i = 0; i < data.length; i++) { + data[i] = _reconstructPacket(data[i], buffers); + } + } + else if (typeof data === "object") { + for (const key in data) { + if (data.hasOwnProperty(key)) { + data[key] = _reconstructPacket(data[key], buffers); + } + } + } + return data; +} diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..0b88a8a --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,71 @@ +import Emitter from "component-emitter"; +/** + * Protocol version. + * + * @public + */ +export declare const protocol: number; +export declare enum PacketType { + CONNECT = 0, + DISCONNECT = 1, + EVENT = 2, + ACK = 3, + ERROR = 4, + BINARY_EVENT = 5, + BINARY_ACK = 6 +} +export interface Packet { + type: PacketType; + nsp: string; + data?: any; + id?: number; + attachments?: number; +} +/** + * A socket.io Encoder instance + */ +export declare class Encoder { + /** + * Encode a packet as a single string if non-binary, or as a + * buffer sequence, depending on packet type. + * + * @param {Object} obj - packet object + */ + encode(obj: Packet): any[]; + /** + * Encode packet as string. + */ + private encodeAsString; + /** + * Encode packet as 'buffer sequence' by removing blobs, and + * deconstructing packet into object with placeholders and + * a list of buffers. + */ + private encodeAsBinary; +} +/** + * A socket.io Decoder instance + * + * @return {Object} decoder + */ +export declare class Decoder extends Emitter { + private reconstructor; + constructor(); + /** + * Decodes an encoded packet string into packet JSON. + * + * @param {String} obj - encoded packet + */ + add(obj: any): void; + /** + * Decode a packet String (JSON data) + * + * @param {String} str + * @return {Object} packet + */ + private decodeString; + /** + * Deallocates a parser's resources + */ + destroy(): void; +} diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..732933d --- /dev/null +++ b/dist/index.js @@ -0,0 +1,268 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Decoder = exports.Encoder = exports.PacketType = exports.protocol = void 0; +const component_emitter_1 = __importDefault(require("component-emitter")); +const binary_1 = require("./binary"); +const is_binary_1 = __importDefault(require("./is-binary")); +const debug_1 = __importDefault(require("debug")); +const debug = debug_1.default("socket.io-parser"); +/** + * Protocol version. + * + * @public + */ +exports.protocol = 4; +var PacketType; +(function (PacketType) { + PacketType[PacketType["CONNECT"] = 0] = "CONNECT"; + PacketType[PacketType["DISCONNECT"] = 1] = "DISCONNECT"; + PacketType[PacketType["EVENT"] = 2] = "EVENT"; + PacketType[PacketType["ACK"] = 3] = "ACK"; + PacketType[PacketType["ERROR"] = 4] = "ERROR"; + PacketType[PacketType["BINARY_EVENT"] = 5] = "BINARY_EVENT"; + PacketType[PacketType["BINARY_ACK"] = 6] = "BINARY_ACK"; +})(PacketType = exports.PacketType || (exports.PacketType = {})); +/** + * A socket.io Encoder instance + */ +class Encoder { + /** + * Encode a packet as a single string if non-binary, or as a + * buffer sequence, depending on packet type. + * + * @param {Object} obj - packet object + */ + encode(obj) { + debug("encoding packet %j", obj); + if (obj.type === PacketType.BINARY_EVENT || + obj.type === PacketType.BINARY_ACK) { + return this.encodeAsBinary(obj); + } + else { + const encoding = this.encodeAsString(obj); + return [encoding]; + } + } + /** + * Encode packet as string. + */ + encodeAsString(obj) { + // first is type + let str = "" + obj.type; + // attachments if we have them + if (obj.type === PacketType.BINARY_EVENT || + obj.type === PacketType.BINARY_ACK) { + str += obj.attachments + "-"; + } + // if we have a namespace other than `/` + // we append it followed by a comma `,` + if (obj.nsp && "/" !== obj.nsp) { + str += obj.nsp + ","; + } + // immediately followed by the id + if (null != obj.id) { + str += obj.id; + } + // json data + if (null != obj.data) { + str += JSON.stringify(obj.data); + } + debug("encoded %j as %s", obj, str); + return str; + } + /** + * Encode packet as 'buffer sequence' by removing blobs, and + * deconstructing packet into object with placeholders and + * a list of buffers. + */ + encodeAsBinary(obj) { + const deconstruction = binary_1.deconstructPacket(obj); + const pack = this.encodeAsString(deconstruction.packet); + const buffers = deconstruction.buffers; + buffers.unshift(pack); // add packet info to beginning of data list + return buffers; // write all the buffers + } +} +exports.Encoder = Encoder; +/** + * A socket.io Decoder instance + * + * @return {Object} decoder + */ +class Decoder extends component_emitter_1.default { + constructor() { + super(); + } + /** + * Decodes an encoded packet string into packet JSON. + * + * @param {String} obj - encoded packet + */ + add(obj) { + let packet; + if (typeof obj === "string") { + packet = this.decodeString(obj); + if (packet.type === PacketType.BINARY_EVENT || + packet.type === PacketType.BINARY_ACK) { + // binary packet's json + this.reconstructor = new BinaryReconstructor(packet); + // no attachments, labeled binary but no binary data to follow + if (packet.attachments === 0) { + super.emit("decoded", packet); + } + } + else { + // non-binary full packet + super.emit("decoded", packet); + } + } + else if (is_binary_1.default(obj) || obj.base64) { + // raw binary data + if (!this.reconstructor) { + throw new Error("got binary data when not reconstructing a packet"); + } + else { + packet = this.reconstructor.takeBinaryData(obj); + if (packet) { + // received final buffer + this.reconstructor = null; + super.emit("decoded", packet); + } + } + } + else { + throw new Error("Unknown type: " + obj); + } + } + /** + * Decode a packet String (JSON data) + * + * @param {String} str + * @return {Object} packet + */ + decodeString(str) { + let i = 0; + // look up type + const p = { + type: Number(str.charAt(0)), + }; + if (PacketType[p.type] === undefined) { + throw new Error("unknown packet type " + p.type); + } + // look up attachments if type binary + if (p.type === PacketType.BINARY_EVENT || + p.type === PacketType.BINARY_ACK) { + const start = i + 1; + while (str.charAt(++i) !== "-" && i != str.length) { } + const buf = str.substring(start, i); + if (buf != Number(buf) || str.charAt(i) !== "-") { + throw new Error("Illegal attachments"); + } + p.attachments = Number(buf); + } + // look up namespace (if any) + if ("/" === str.charAt(i + 1)) { + const start = i + 1; + while (++i) { + const c = str.charAt(i); + if ("," === c) + break; + if (i === str.length) + break; + } + p.nsp = str.substring(start, i); + } + else { + p.nsp = "/"; + } + // look up id + const next = str.charAt(i + 1); + if ("" !== next && Number(next) == next) { + const start = i + 1; + while (++i) { + const c = str.charAt(i); + if (null == c || Number(c) != c) { + --i; + break; + } + if (i === str.length) + break; + } + p.id = Number(str.substring(start, i + 1)); + } + // look up json data + if (str.charAt(++i)) { + const payload = tryParse(str.substr(i)); + const isPayloadValid = payload !== false && + (p.type === PacketType.ERROR || Array.isArray(payload)); + if (isPayloadValid) { + p.data = payload; + } + else { + throw new Error("invalid payload"); + } + } + debug("decoded %s as %j", str, p); + return p; + } + /** + * Deallocates a parser's resources + */ + destroy() { + if (this.reconstructor) { + this.reconstructor.finishedReconstruction(); + } + } +} +exports.Decoder = Decoder; +function tryParse(str) { + try { + return JSON.parse(str); + } + catch (e) { + return false; + } +} +/** + * A manager of a binary event's 'buffer sequence'. Should + * be constructed whenever a packet of type BINARY_EVENT is + * decoded. + * + * @param {Object} packet + * @return {BinaryReconstructor} initialized reconstructor + */ +class BinaryReconstructor { + constructor(packet) { + this.packet = packet; + this.buffers = []; + this.reconPack = packet; + } + /** + * Method to be called when binary data received from connection + * after a BINARY_EVENT packet. + * + * @param {Buffer | ArrayBuffer} binData - the raw binary data received + * @return {null | Object} returns null if more binary data is expected or + * a reconstructed packet object if all buffers have been received. + */ + takeBinaryData(binData) { + this.buffers.push(binData); + if (this.buffers.length === this.reconPack.attachments) { + // done with buffer list + const packet = binary_1.reconstructPacket(this.reconPack, this.buffers); + this.finishedReconstruction(); + return packet; + } + return null; + } + /** + * Cleans up binary packet reconstruction variables. + */ + finishedReconstruction() { + this.reconPack = null; + this.buffers = []; + } +} diff --git a/dist/is-binary.d.ts b/dist/is-binary.d.ts new file mode 100644 index 0000000..9b284a8 --- /dev/null +++ b/dist/is-binary.d.ts @@ -0,0 +1,6 @@ +/** + * Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File. + * + * @private + */ +export default function isBinary(obj: any): boolean; diff --git a/dist/is-binary.js b/dist/is-binary.js new file mode 100644 index 0000000..41c3f42 --- /dev/null +++ b/dist/is-binary.js @@ -0,0 +1,28 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const withNativeBuffer = typeof Buffer === "function" && typeof Buffer.isBuffer === "function"; +const withNativeArrayBuffer = typeof ArrayBuffer === "function"; +const isView = (obj) => { + return typeof ArrayBuffer.isView === "function" + ? ArrayBuffer.isView(obj) + : obj.buffer instanceof ArrayBuffer; +}; +const toString = Object.prototype.toString; +const withNativeBlob = typeof Blob === "function" || + (typeof Blob !== "undefined" && + toString.call(Blob) === "[object BlobConstructor]"); +const withNativeFile = typeof File === "function" || + (typeof File !== "undefined" && + toString.call(File) === "[object FileConstructor]"); +/** + * Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File. + * + * @private + */ +function isBinary(obj) { + return ((withNativeBuffer && Buffer.isBuffer(obj)) || + (withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) || + (withNativeBlob && obj instanceof Blob) || + (withNativeFile && obj instanceof File)); +} +exports.default = isBinary;