Bug 1891710: part 2) Enable <Element-outerHTML.html> WPT for Trusted Types. r=smaug
[gecko.git] / devtools / shared / transport / packets.js
blobe4b85697562793656b97034c572ebcb56396ed26
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 /**
8  * Packets contain read / write functionality for the different packet types
9  * supported by the debugging protocol, so that a transport can focus on
10  * delivery and queue management without worrying too much about the specific
11  * packet types.
12  *
13  * They are intended to be "one use only", so a new packet should be
14  * instantiated for each incoming or outgoing packet.
15  *
16  * A complete Packet type should expose at least the following:
17  *   * read(stream, scriptableStream)
18  *     Called when the input stream has data to read
19  *   * write(stream)
20  *     Called when the output stream is ready to write
21  *   * get done()
22  *     Returns true once the packet is done being read / written
23  *   * destroy()
24  *     Called to clean up at the end of use
25  */
27 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
28 const { dumpn, dumpv } = DevToolsUtils;
29 const flags = require("resource://devtools/shared/flags.js");
30 const StreamUtils = require("resource://devtools/shared/transport/stream-utils.js");
32 DevToolsUtils.defineLazyGetter(this, "unicodeConverter", () => {
33   // eslint-disable-next-line no-shadow
34   const unicodeConverter = Cc[
35     "@mozilla.org/intl/scriptableunicodeconverter"
36   ].createInstance(Ci.nsIScriptableUnicodeConverter);
37   unicodeConverter.charset = "UTF-8";
38   return unicodeConverter;
39 });
41 // The transport's previous check ensured the header length did not exceed 20
42 // characters.  Here, we opt for the somewhat smaller, but still large limit of
43 // 1 TiB.
44 const PACKET_LENGTH_MAX = Math.pow(2, 40);
46 /**
47  * A generic Packet processing object (extended by two subtypes below).
48  */
49 function Packet(transport) {
50   this._transport = transport;
51   this._length = 0;
54 /**
55  * Attempt to initialize a new Packet based on the incoming packet header we've
56  * received so far.  We try each of the types in succession, trying JSON packets
57  * first since they are much more common.
58  * @param header string
59  *        The packet header string to attempt parsing.
60  * @param transport DebuggerTransport
61  *        The transport instance that will own the packet.
62  * @return Packet
63  *         The parsed packet of the matching type, or null if no types matched.
64  */
65 Packet.fromHeader = function (header, transport) {
66   return (
67     JSONPacket.fromHeader(header, transport) ||
68     BulkPacket.fromHeader(header, transport)
69   );
72 Packet.prototype = {
73   get length() {
74     return this._length;
75   },
77   set length(length) {
78     if (length > PACKET_LENGTH_MAX) {
79       throw Error(
80         "Packet length " +
81           length +
82           " exceeds the max length of " +
83           PACKET_LENGTH_MAX
84       );
85     }
86     this._length = length;
87   },
89   destroy() {
90     this._transport = null;
91   },
94 exports.Packet = Packet;
96 /**
97  * With a JSON packet (the typical packet type sent via the transport), data is
98  * transferred as a JSON packet serialized into a string, with the string length
99  * prepended to the packet, followed by a colon ([length]:[packet]). The
100  * contents of the JSON packet are specified in the Remote Debugging Protocol
101  * specification.
102  * @param transport DebuggerTransport
103  *        The transport instance that will own the packet.
104  */
105 function JSONPacket(transport) {
106   Packet.call(this, transport);
107   this._data = "";
108   this._done = false;
112  * Attempt to initialize a new JSONPacket based on the incoming packet header
113  * we've received so far.
114  * @param header string
115  *        The packet header string to attempt parsing.
116  * @param transport DebuggerTransport
117  *        The transport instance that will own the packet.
118  * @return JSONPacket
119  *         The parsed packet, or null if it's not a match.
120  */
121 JSONPacket.fromHeader = function (header, transport) {
122   const match = this.HEADER_PATTERN.exec(header);
124   if (!match) {
125     return null;
126   }
128   dumpv("Header matches JSON packet");
129   const packet = new JSONPacket(transport);
130   packet.length = +match[1];
131   return packet;
134 JSONPacket.HEADER_PATTERN = /^(\d+):$/;
136 JSONPacket.prototype = Object.create(Packet.prototype);
138 Object.defineProperty(JSONPacket.prototype, "object", {
139   /**
140    * Gets the object (not the serialized string) being read or written.
141    */
142   get() {
143     return this._object;
144   },
146   /**
147    * Sets the object to be sent when write() is called.
148    */
149   set(object) {
150     this._object = object;
151     const data = JSON.stringify(object);
152     this._data = unicodeConverter.ConvertFromUnicode(data);
153     this.length = this._data.length;
154   },
157 JSONPacket.prototype.read = function (stream, scriptableStream) {
158   dumpv("Reading JSON packet");
160   // Read in more packet data.
161   this._readData(stream, scriptableStream);
163   if (!this.done) {
164     // Don't have a complete packet yet.
165     return;
166   }
168   let json = this._data;
169   try {
170     json = unicodeConverter.ConvertToUnicode(json);
171     this._object = JSON.parse(json);
172   } catch (e) {
173     const msg =
174       "Error parsing incoming packet: " +
175       json +
176       " (" +
177       e +
178       " - " +
179       e.stack +
180       ")";
181     console.error(msg);
182     dumpn(msg);
183     return;
184   }
186   this._transport._onJSONObjectReady(this._object);
189 JSONPacket.prototype._readData = function (stream, scriptableStream) {
190   if (flags.wantVerbose) {
191     dumpv(
192       "Reading JSON data: _l: " +
193         this.length +
194         " dL: " +
195         this._data.length +
196         " sA: " +
197         stream.available()
198     );
199   }
200   const bytesToRead = Math.min(
201     this.length - this._data.length,
202     stream.available()
203   );
204   this._data += scriptableStream.readBytes(bytesToRead);
205   this._done = this._data.length === this.length;
208 JSONPacket.prototype.write = function (stream) {
209   dumpv("Writing JSON packet");
211   if (this._outgoing === undefined) {
212     // Format the serialized packet to a buffer
213     this._outgoing = this.length + ":" + this._data;
214   }
216   const written = stream.write(this._outgoing, this._outgoing.length);
217   this._outgoing = this._outgoing.slice(written);
218   this._done = !this._outgoing.length;
221 Object.defineProperty(JSONPacket.prototype, "done", {
222   get() {
223     return this._done;
224   },
227 JSONPacket.prototype.toString = function () {
228   return JSON.stringify(this._object, null, 2);
231 exports.JSONPacket = JSONPacket;
234  * With a bulk packet, data is transferred by temporarily handing over the
235  * transport's input or output stream to the application layer for writing data
236  * directly.  This can be much faster for large data sets, and avoids various
237  * stages of copies and data duplication inherent in the JSON packet type.  The
238  * bulk packet looks like:
240  * bulk [actor] [type] [length]:[data]
242  * The interpretation of the data portion depends on the kind of actor and the
243  * packet's type.  See the Remote Debugging Protocol Stream Transport spec for
244  * more details.
245  * @param transport DebuggerTransport
246  *        The transport instance that will own the packet.
247  */
248 function BulkPacket(transport) {
249   Packet.call(this, transport);
250   this._done = false;
251   let _resolve;
252   this._readyForWriting = new Promise(resolve => {
253     _resolve = resolve;
254   });
255   this._readyForWriting.resolve = _resolve;
259  * Attempt to initialize a new BulkPacket based on the incoming packet header
260  * we've received so far.
261  * @param header string
262  *        The packet header string to attempt parsing.
263  * @param transport DebuggerTransport
264  *        The transport instance that will own the packet.
265  * @return BulkPacket
266  *         The parsed packet, or null if it's not a match.
267  */
268 BulkPacket.fromHeader = function (header, transport) {
269   const match = this.HEADER_PATTERN.exec(header);
271   if (!match) {
272     return null;
273   }
275   dumpv("Header matches bulk packet");
276   const packet = new BulkPacket(transport);
277   packet.header = {
278     actor: match[1],
279     type: match[2],
280     length: +match[3],
281   };
282   return packet;
285 BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
287 BulkPacket.prototype = Object.create(Packet.prototype);
289 BulkPacket.prototype.read = function (stream) {
290   dumpv("Reading bulk packet, handing off input stream");
292   // Temporarily pause monitoring of the input stream
293   this._transport.pauseIncoming();
295   new Promise(resolve => {
296     this._transport._onBulkReadReady({
297       actor: this.actor,
298       type: this.type,
299       length: this.length,
300       copyTo: output => {
301         dumpv("CT length: " + this.length);
302         const copying = StreamUtils.copyStream(stream, output, this.length);
303         resolve(copying);
304         return copying;
305       },
306       stream,
307       done: resolve,
308     });
309     // Await the result of reading from the stream
310   }).then(() => {
311     dumpv("onReadDone called, ending bulk mode");
312     this._done = true;
313     this._transport.resumeIncoming();
314   }, this._transport.close);
316   // Ensure this is only done once
317   this.read = () => {
318     throw new Error("Tried to read() a BulkPacket's stream multiple times.");
319   };
322 BulkPacket.prototype.write = function (stream) {
323   dumpv("Writing bulk packet");
325   if (this._outgoingHeader === undefined) {
326     dumpv("Serializing bulk packet header");
327     // Format the serialized packet header to a buffer
328     this._outgoingHeader =
329       "bulk " + this.actor + " " + this.type + " " + this.length + ":";
330   }
332   // Write the header, or whatever's left of it to write.
333   if (this._outgoingHeader.length) {
334     dumpv("Writing bulk packet header");
335     const written = stream.write(
336       this._outgoingHeader,
337       this._outgoingHeader.length
338     );
339     this._outgoingHeader = this._outgoingHeader.slice(written);
340     return;
341   }
343   dumpv("Handing off output stream");
345   // Temporarily pause the monitoring of the output stream
346   this._transport.pauseOutgoing();
348   new Promise(resolve => {
349     this._readyForWriting.resolve({
350       copyFrom: input => {
351         dumpv("CF length: " + this.length);
352         const copying = StreamUtils.copyStream(input, stream, this.length);
353         resolve(copying);
354         return copying;
355       },
356       stream,
357       done: resolve,
358     });
359     // Await the result of writing to the stream
360   }).then(() => {
361     dumpv("onWriteDone called, ending bulk mode");
362     this._done = true;
363     this._transport.resumeOutgoing();
364   }, this._transport.close);
366   // Ensure this is only done once
367   this.write = () => {
368     throw new Error("Tried to write() a BulkPacket's stream multiple times.");
369   };
372 Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
373   get() {
374     return this._readyForWriting;
375   },
378 Object.defineProperty(BulkPacket.prototype, "header", {
379   get() {
380     return {
381       actor: this.actor,
382       type: this.type,
383       length: this.length,
384     };
385   },
387   set(header) {
388     this.actor = header.actor;
389     this.type = header.type;
390     this.length = header.length;
391   },
394 Object.defineProperty(BulkPacket.prototype, "done", {
395   get() {
396     return this._done;
397   },
400 BulkPacket.prototype.toString = function () {
401   return "Bulk: " + JSON.stringify(this.header, null, 2);
404 exports.BulkPacket = BulkPacket;
407  * RawPacket is used to test the transport's error handling of malformed
408  * packets, by writing data directly onto the stream.
409  * @param transport DebuggerTransport
410  *        The transport instance that will own the packet.
411  * @param data string
412  *        The raw string to send out onto the stream.
413  */
414 function RawPacket(transport, data) {
415   Packet.call(this, transport);
416   this._data = data;
417   this.length = data.length;
418   this._done = false;
421 RawPacket.prototype = Object.create(Packet.prototype);
423 RawPacket.prototype.read = function () {
424   // This hasn't yet been needed for testing.
425   throw Error("Not implmented.");
428 RawPacket.prototype.write = function (stream) {
429   const written = stream.write(this._data, this._data.length);
430   this._data = this._data.slice(written);
431   this._done = !this._data.length;
434 Object.defineProperty(RawPacket.prototype, "done", {
435   get() {
436     return this._done;
437   },
440 exports.RawPacket = RawPacket;