/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const EXPORTED_SYMBOLS = ["RawPacket", "Packet", "JSONPacket", "BulkPacket"]; const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); XPCOMUtils.defineLazyModuleGetters(this, { StreamUtils: "chrome://remote/content/marionette/stream-utils.js", }); XPCOMUtils.defineLazyGetter(this, "unicodeConverter", () => { const unicodeConverter = Cc[ "@mozilla.org/intl/scriptableunicodeconverter" ].createInstance(Ci.nsIScriptableUnicodeConverter); unicodeConverter.charset = "UTF-8"; return unicodeConverter; }); /** * Packets contain read / write functionality for the different packet types * supported by the debugging protocol, so that a transport can focus on * delivery and queue management without worrying too much about the specific * packet types. * * They are intended to be "one use only", so a new packet should be * instantiated for each incoming or outgoing packet. * * A complete Packet type should expose at least the following: * * read(stream, scriptableStream) * Called when the input stream has data to read * * write(stream) * Called when the output stream is ready to write * * get done() * Returns true once the packet is done being read / written * * destroy() * Called to clean up at the end of use */ const defer = function() { let deferred = { promise: new Promise((resolve, reject) => { deferred.resolve = resolve; deferred.reject = reject; }), }; return deferred; }; // The transport's previous check ensured the header length did not // exceed 20 characters. Here, we opt for the somewhat smaller, but still // large limit of 1 TiB. const PACKET_LENGTH_MAX = Math.pow(2, 40); /** * A generic Packet processing object (extended by two subtypes below). * * @class */ function Packet(transport) { this._transport = transport; this._length = 0; } /** * Attempt to initialize a new Packet based on the incoming packet header * we've received so far. We try each of the types in succession, trying * JSON packets first since they are much more common. * * @param {string} header * Packet header string to attempt parsing. * @param {DebuggerTransport} transport * Transport instance that will own the packet. * * @return {Packet} * Parsed packet of the matching type, or null if no types matched. */ Packet.fromHeader = function(header, transport) { return ( JSONPacket.fromHeader(header, transport) || BulkPacket.fromHeader(header, transport) ); }; Packet.prototype = { get length() { return this._length; }, set length(length) { if (length > PACKET_LENGTH_MAX) { throw new Error( "Packet length " + length + " exceeds the max length of " + PACKET_LENGTH_MAX ); } this._length = length; }, destroy() { this._transport = null; }, }; /** * With a JSON packet (the typical packet type sent via the transport), * data is transferred as a JSON packet serialized into a string, * with the string length prepended to the packet, followed by a colon * ([length]:[packet]). The contents of the JSON packet are specified in * the Remote Debugging Protocol specification. * * @param {DebuggerTransport} transport * Transport instance that will own the packet. */ function JSONPacket(transport) { Packet.call(this, transport); this._data = ""; this._done = false; } /** * Attempt to initialize a new JSONPacket based on the incoming packet * header we've received so far. * * @param {string} header * Packet header string to attempt parsing. * @param {DebuggerTransport} transport * Transport instance that will own the packet. * * @return {JSONPacket} * Parsed packet, or null if it's not a match. */ JSONPacket.fromHeader = function(header, transport) { let match = this.HEADER_PATTERN.exec(header); if (!match) { return null; } let packet = new JSONPacket(transport); packet.length = +match[1]; return packet; }; JSONPacket.HEADER_PATTERN = /^(\d+):$/; JSONPacket.prototype = Object.create(Packet.prototype); Object.defineProperty(JSONPacket.prototype, "object", { /** * Gets the object (not the serialized string) being read or written. */ get() { return this._object; }, /** * Sets the object to be sent when write() is called. */ set(object) { this._object = object; let data = JSON.stringify(object); this._data = unicodeConverter.ConvertFromUnicode(data); this.length = this._data.length; }, }); JSONPacket.prototype.read = function(stream, scriptableStream) { // Read in more packet data. this._readData(stream, scriptableStream); if (!this.done) { // Don't have a complete packet yet. return; } let json = this._data; try { json = unicodeConverter.ConvertToUnicode(json); this._object = JSON.parse(json); } catch (e) { let msg = "Error parsing incoming packet: " + json + " (" + e + " - " + e.stack + ")"; console.error(msg); dump(msg + "\n"); return; } this._transport._onJSONObjectReady(this._object); }; JSONPacket.prototype._readData = function(stream, scriptableStream) { let bytesToRead = Math.min( this.length - this._data.length, stream.available() ); this._data += scriptableStream.readBytes(bytesToRead); this._done = this._data.length === this.length; }; JSONPacket.prototype.write = function(stream) { if (this._outgoing === undefined) { // Format the serialized packet to a buffer this._outgoing = this.length + ":" + this._data; } let written = stream.write(this._outgoing, this._outgoing.length); this._outgoing = this._outgoing.slice(written); this._done = !this._outgoing.length; }; Object.defineProperty(JSONPacket.prototype, "done", { get() { return this._done; }, }); JSONPacket.prototype.toString = function() { return JSON.stringify(this._object, null, 2); }; /** * With a bulk packet, data is transferred by temporarily handing over * the transport's input or output stream to the application layer for * writing data directly. This can be much faster for large data sets, * and avoids various stages of copies and data duplication inherent in * the JSON packet type. The bulk packet looks like: * * bulk [actor] [type] [length]:[data] * * The interpretation of the data portion depends on the kind of actor and * the packet's type. See the Remote Debugging Protocol Stream Transport * spec for more details. * * @param {DebuggerTransport} transport * Transport instance that will own the packet. */ function BulkPacket(transport) { Packet.call(this, transport); this._done = false; this._readyForWriting = defer(); } /** * Attempt to initialize a new BulkPacket based on the incoming packet * header we've received so far. * * @param {string} header * Packet header string to attempt parsing. * @param {DebuggerTransport} transport * Transport instance that will own the packet. * * @return {BulkPacket} * Parsed packet, or null if it's not a match. */ BulkPacket.fromHeader = function(header, transport) { let match = this.HEADER_PATTERN.exec(header); if (!match) { return null; } let packet = new BulkPacket(transport); packet.header = { actor: match[1], type: match[2], length: +match[3], }; return packet; }; BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/; BulkPacket.prototype = Object.create(Packet.prototype); BulkPacket.prototype.read = function(stream) { // Temporarily pause monitoring of the input stream this._transport.pauseIncoming(); let deferred = defer(); this._transport._onBulkReadReady({ actor: this.actor, type: this.type, length: this.length, copyTo: output => { let copying = StreamUtils.copyStream(stream, output, this.length); deferred.resolve(copying); return copying; }, stream, done: deferred, }); // Await the result of reading from the stream deferred.promise.then(() => { this._done = true; this._transport.resumeIncoming(); }, this._transport.close); // Ensure this is only done once this.read = () => { throw new Error("Tried to read() a BulkPacket's stream multiple times."); }; }; BulkPacket.prototype.write = function(stream) { if (this._outgoingHeader === undefined) { // Format the serialized packet header to a buffer this._outgoingHeader = "bulk " + this.actor + " " + this.type + " " + this.length + ":"; } // Write the header, or whatever's left of it to write. if (this._outgoingHeader.length) { let written = stream.write( this._outgoingHeader, this._outgoingHeader.length ); this._outgoingHeader = this._outgoingHeader.slice(written); return; } // Temporarily pause the monitoring of the output stream this._transport.pauseOutgoing(); let deferred = defer(); this._readyForWriting.resolve({ copyFrom: input => { let copying = StreamUtils.copyStream(input, stream, this.length); deferred.resolve(copying); return copying; }, stream, done: deferred, }); // Await the result of writing to the stream deferred.promise.then(() => { this._done = true; this._transport.resumeOutgoing(); }, this._transport.close); // Ensure this is only done once this.write = () => { throw new Error("Tried to write() a BulkPacket's stream multiple times."); }; }; Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", { get() { return this._readyForWriting.promise; }, }); Object.defineProperty(BulkPacket.prototype, "header", { get() { return { actor: this.actor, type: this.type, length: this.length, }; }, set(header) { this.actor = header.actor; this.type = header.type; this.length = header.length; }, }); Object.defineProperty(BulkPacket.prototype, "done", { get() { return this._done; }, }); BulkPacket.prototype.toString = function() { return "Bulk: " + JSON.stringify(this.header, null, 2); }; /** * RawPacket is used to test the transport's error handling of malformed * packets, by writing data directly onto the stream. * @param transport DebuggerTransport * The transport instance that will own the packet. * @param data string * The raw string to send out onto the stream. */ function RawPacket(transport, data) { Packet.call(this, transport); this._data = data; this.length = data.length; this._done = false; } RawPacket.prototype = Object.create(Packet.prototype); RawPacket.prototype.read = function() { // this has not yet been needed for testing throw new Error("Not implemented"); }; RawPacket.prototype.write = function(stream) { let written = stream.write(this._data, this._data.length); this._data = this._data.slice(written); this._done = !this._data.length; }; Object.defineProperty(RawPacket.prototype, "done", { get() { return this._done; }, });