tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

types.js (18148B)


      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/. */
      4 
      5 "use strict";
      6 
      7 var { Actor } = require("resource://devtools/shared/protocol/Actor.js");
      8 var {
      9  lazyLoadSpec,
     10  lazyLoadFront,
     11 } = require("resource://devtools/shared/specs/index.js");
     12 
     13 /**
     14 * Types: named marshallers/demarshallers.
     15 *
     16 * Types provide a 'write' function that takes a js representation and
     17 * returns a protocol representation, and a "read" function that
     18 * takes a protocol representation and returns a js representation.
     19 *
     20 * The read and write methods are also passed a context object that
     21 * represent the actor or front requesting the translation.
     22 *
     23 * Types are referred to with a typestring.  Basic types are
     24 * registered by name using addType, and more complex types can
     25 * be generated by adding detail to the type name.
     26 */
     27 
     28 var types = Object.create(null);
     29 exports.types = types;
     30 
     31 var registeredTypes = (types.registeredTypes = new Map());
     32 
     33 exports.registeredTypes = registeredTypes;
     34 
     35 // Values used in specification's request or response attributes to specify
     36 // if a request should only send (request) orreceive (response) raw byte thanks to bulk requests.
     37 // In such case, the front/actor can't send/receive any custom json attribute.
     38 // The request will result into a one-way JSON packet with "actor", "type" and "length" attributes only.
     39 // (i.e. there is no response packet as being one-way)
     40 exports.BULK_REQUEST = Symbol("request");
     41 exports.BULK_RESPONSE = Symbol("response");
     42 
     43 /**
     44 * Return the type object associated with a given typestring.
     45 * If passed a type object, it will be returned unchanged.
     46 *
     47 * Types can be registered with addType, or can be created on
     48 * the fly with typestrings.  Examples:
     49 *
     50 *   boolean
     51 *   threadActor
     52 *   threadActor#detail
     53 *   array:threadActor
     54 *   array:array:threadActor#detail
     55 *
     56 * @param [typestring|type] type
     57 *    Either a typestring naming a type or a type object.
     58 *
     59 * @returns a type object.
     60 */
     61 types.getType = function (type) {
     62  if (!type) {
     63    return types.Primitive;
     64  }
     65 
     66  if (typeof type !== "string") {
     67    return type;
     68  }
     69 
     70  // If already registered, we're done here.
     71  let reg = registeredTypes.get(type);
     72  if (reg) {
     73    return reg;
     74  }
     75 
     76  // Try to lazy load the spec, if not already loaded.
     77  if (lazyLoadSpec(type)) {
     78    // If a spec module was lazy loaded, it will synchronously call
     79    // generateActorSpec, and set the type in `registeredTypes`.
     80    reg = registeredTypes.get(type);
     81    if (reg) {
     82      return reg;
     83    }
     84  }
     85 
     86  // New type, see if it's a collection type:
     87  const sep = type.indexOf(":");
     88  if (sep >= 0) {
     89    const collection = type.substring(0, sep);
     90    const subtype = types.getType(type.substring(sep + 1));
     91 
     92    if (collection === "array") {
     93      return types.addArrayType(subtype);
     94    } else if (collection === "nullable") {
     95      return types.addNullableType(subtype);
     96    }
     97 
     98    throw Error("Unknown collection type: " + collection);
     99  }
    100 
    101  // Not a collection, might be actor detail
    102  const pieces = type.split("#", 2);
    103  if (pieces.length > 1) {
    104    if (pieces[1] != "actorid") {
    105      throw new Error(
    106        "Unsupported detail, only support 'actorid', got: " + pieces[1]
    107      );
    108    }
    109    return types.addActorDetail(type, pieces[0], pieces[1]);
    110  }
    111 
    112  throw Error("Unknown type: " + type);
    113 };
    114 
    115 /**
    116 * Don't allow undefined when writing primitive types to packets.  If
    117 * you want to allow undefined, use a nullable type.
    118 */
    119 function identityWrite(v) {
    120  if (v === undefined) {
    121    throw Error("undefined passed where a value is required");
    122  }
    123  // This has to handle iterator->array conversion because arrays of
    124  // primitive types pass through here.
    125  if (v && typeof v.next === "function") {
    126    return [...v];
    127  }
    128  return v;
    129 }
    130 
    131 /**
    132 * Add a type to the type system.
    133 *
    134 * When registering a type, you can provide `read` and `write` methods.
    135 *
    136 * The `read` method will be passed a JS object value from the JSON
    137 * packet and must return a native representation.  The `write` method will
    138 * be passed a native representation and should provide a JSONable value.
    139 *
    140 * These methods will both be passed a context.  The context is the object
    141 * performing or servicing the request - on the server side it will be
    142 * an Actor, on the client side it will be a Front.
    143 *
    144 * @param typestring name
    145 *    Name to register
    146 * @param object typeObject
    147 *    An object whose properties will be stored in the type, including
    148 *    the `read` and `write` methods.
    149 *
    150 * @returns a type object that can be used in protocol definitions.
    151 */
    152 types.addType = function (name, typeObject = {}) {
    153  if (registeredTypes.has(name)) {
    154    throw Error("Type '" + name + "' already exists.");
    155  }
    156 
    157  const type = Object.assign(
    158    {
    159      toString() {
    160        return "[protocol type:" + name + "]";
    161      },
    162      name,
    163      primitive: !(typeObject.read || typeObject.write),
    164      read: identityWrite,
    165      write: identityWrite,
    166    },
    167    typeObject
    168  );
    169 
    170  registeredTypes.set(name, type);
    171 
    172  return type;
    173 };
    174 
    175 /**
    176 * Remove a type previously registered with the system.
    177 * Primarily useful for types registered by addons.
    178 */
    179 types.removeType = function (name) {
    180  // This type may still be referenced by other types, make sure
    181  // those references don't work.
    182  const type = registeredTypes.get(name);
    183 
    184  type.name = "DEFUNCT:" + name;
    185  type.category = "defunct";
    186  type.primitive = false;
    187  type.read = type.write = function () {
    188    throw new Error("Using defunct type: " + name);
    189  };
    190 
    191  registeredTypes.delete(name);
    192 };
    193 
    194 /**
    195 * Add an array type to the type system.
    196 *
    197 * getType() will call this function if provided an "array:<type>"
    198 * typestring.
    199 *
    200 * @param type subtype
    201 *    The subtype to be held by the array.
    202 */
    203 types.addArrayType = function (subtype) {
    204  subtype = types.getType(subtype);
    205 
    206  const name = "array:" + subtype.name;
    207 
    208  // Arrays of primitive types are primitive types themselves.
    209  if (subtype.primitive) {
    210    return types.addType(name);
    211  }
    212  return types.addType(name, {
    213    category: "array",
    214    read: (v, ctx) => {
    215      if (v && typeof v.next === "function") {
    216        v = [...v];
    217      }
    218      return v.map(i => subtype.read(i, ctx));
    219    },
    220    write: (v, ctx) => {
    221      if (v && typeof v.next === "function") {
    222        v = [...v];
    223      }
    224      return v.map(i => subtype.write(i, ctx));
    225    },
    226  });
    227 };
    228 
    229 /**
    230 * Add a dict type to the type system.  This allows you to serialize
    231 * a JS object that contains non-primitive subtypes.
    232 *
    233 * Properties of the value that aren't included in the specializations
    234 * will be serialized as primitive values.
    235 *
    236 * @param object specializations
    237 *    A dict of property names => type
    238 */
    239 types.addDictType = function (name, specializations) {
    240  const specTypes = {};
    241  for (const prop in specializations) {
    242    try {
    243      specTypes[prop] = types.getType(specializations[prop]);
    244    } catch (e) {
    245      // Types may not be defined yet. Sometimes, we define the type *after* using it, but
    246      // also, we have cyclic definitions on types. So lazily load them when they are not
    247      // immediately available.
    248      loader.lazyGetter(specTypes, prop, () => {
    249        return types.getType(specializations[prop]);
    250      });
    251    }
    252  }
    253  return types.addType(name, {
    254    category: "dict",
    255    specializations,
    256    read: (v, ctx) => {
    257      const ret = {};
    258      for (const prop in v) {
    259        if (prop in specTypes) {
    260          ret[prop] = specTypes[prop].read(v[prop], ctx);
    261        } else {
    262          ret[prop] = v[prop];
    263        }
    264      }
    265      return ret;
    266    },
    267 
    268    write: (v, ctx) => {
    269      const ret = {};
    270      for (const prop in v) {
    271        if (prop in specTypes) {
    272          ret[prop] = specTypes[prop].write(v[prop], ctx);
    273        } else {
    274          ret[prop] = v[prop];
    275        }
    276      }
    277      return ret;
    278    },
    279  });
    280 };
    281 
    282 /**
    283 * Register an actor type with the type system.
    284 *
    285 * Types are marshalled differently when communicating server->client
    286 * than they are when communicating client->server.  The server needs
    287 * to provide useful information to the client, so uses the actor's
    288 * `form` method to get a json representation of the actor.  When
    289 * making a request from the client we only need the actor ID string.
    290 *
    291 * This function can be called before the associated actor has been
    292 * constructed, but the read and write methods won't work until
    293 * the associated addActorImpl or addActorFront methods have been
    294 * called during actor/front construction.
    295 *
    296 * @param string name
    297 *    The typestring to register.
    298 */
    299 types.addActorType = function (name) {
    300  // We call addActorType from:
    301  //   FrontClassWithSpec when registering front synchronously,
    302  //   generateActorSpec when defining specs,
    303  //   specs modules to register actor type early to use them in other types
    304  if (registeredTypes.has(name)) {
    305    return registeredTypes.get(name);
    306  }
    307  const type = types.addType(name, {
    308    _actor: true,
    309    category: "actor",
    310    read: (v, ctx, detail) => {
    311      // If we're reading a request on the server side, just
    312      // find the actor registered with this actorID.
    313      if (ctx instanceof Actor) {
    314        return ctx.conn.getActor(v);
    315      }
    316 
    317      // Reading a response on the client side, check for an
    318      // existing front on the connection, and create the front
    319      // if it isn't found.
    320      const actorID = typeof v === "string" ? v : v.actor;
    321      // `ctx.conn` is a DevToolsClient
    322      let front = ctx.conn.getFrontByID(actorID);
    323 
    324      // When the type `${name}#actorid` is used, `v` is a string refering to the
    325      // actor ID. We cannot read form information in this case and the actorID was
    326      // already set when creating the front, so no need to do anything.
    327      let form = null;
    328      if (detail != "actorid") {
    329        form = identityWrite(v);
    330      }
    331 
    332      if (!front) {
    333        // If front isn't instantiated yet, create one.
    334        // Try lazy loading front if not already loaded.
    335        // The front module will synchronously call `FrontClassWithSpec` and
    336        // augment `type` with the `frontClass` attribute.
    337        if (!type.frontClass) {
    338          lazyLoadFront(name);
    339        }
    340 
    341        const parentFront = ctx.marshallPool();
    342        const targetFront = parentFront.isTargetFront
    343          ? parentFront
    344          : parentFront.targetFront;
    345 
    346        // Use intermediate Class variable to please eslint requiring
    347        // a capital letter for all constructors.
    348        const Class = type.frontClass;
    349        front = new Class(ctx.conn, targetFront, parentFront);
    350        front.actorID = actorID;
    351 
    352        parentFront.manage(front, form, ctx);
    353      } else if (form) {
    354        front.form(form, ctx);
    355      }
    356 
    357      return front;
    358    },
    359    write: (v, ctx, detail) => {
    360      // If returning a response from the server side, make sure
    361      // the actor is added to a parent object and return its form.
    362      if (v instanceof Actor) {
    363        if (v.isDestroyed()) {
    364          throw new Error(
    365            `Attempted to write a response containing a destroyed actor`
    366          );
    367        }
    368        if (!v.actorID) {
    369          ctx.marshallPool().manage(v);
    370        }
    371        if (detail == "actorid") {
    372          return v.actorID;
    373        }
    374        return identityWrite(v.form(detail));
    375      }
    376 
    377      // Writing a request from the client side, just send the actor id.
    378      return v.actorID;
    379    },
    380  });
    381  return type;
    382 };
    383 
    384 types.addPolymorphicType = function (name, subtypes) {
    385  // Assert that all subtypes are actors, as the marshalling implementation depends on that.
    386  for (const subTypeName of subtypes) {
    387    const subtype = types.getType(subTypeName);
    388    if (subtype.category != "actor") {
    389      throw new Error(
    390        `In polymorphic type '${subtypes.join(
    391          ","
    392        )}', the type '${subTypeName}' isn't an actor`
    393      );
    394    }
    395  }
    396 
    397  return types.addType(name, {
    398    category: "polymorphic",
    399    read: (value, ctx) => {
    400      // `value` is either a string which is an Actor ID or a form object
    401      // where `actor` is an actor ID
    402      const actorID = typeof value === "string" ? value : value.actor;
    403      if (!actorID) {
    404        throw new Error(
    405          `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'`
    406        );
    407      }
    408 
    409      // Extract the typeName out of the actor ID, which should be composed like this
    410      // ${DevToolsServerConnectionPrefix}.${typeName}${Number}
    411      const typeName = actorID.match(/\.([a-zA-Z]+)\d+$/)[1];
    412      if (!subtypes.includes(typeName)) {
    413        throw new Error(
    414          `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
    415        );
    416      }
    417 
    418      const subtype = types.getType(typeName);
    419      return subtype.read(value, ctx);
    420    },
    421    write: (value, ctx) => {
    422      if (!value) {
    423        throw new Error(
    424          `Was expecting one of these actors '${subtypes}' but instead got an empty value.`
    425        );
    426      }
    427      // value is either an `Actor` or a `Front` and both classes exposes a `typeName`
    428      const typeName = value.typeName;
    429      if (!typeName) {
    430        throw new Error(
    431          `Was expecting one of these actors '${subtypes}' but instead got value: '${value}'. Did you pass a form instead of an Actor?`
    432        );
    433      }
    434 
    435      if (!subtypes.includes(typeName)) {
    436        throw new Error(
    437          `Was expecting one of these actors '${subtypes}' but instead got an actor of type: '${typeName}'`
    438        );
    439      }
    440 
    441      const subtype = types.getType(typeName);
    442      return subtype.write(value, ctx);
    443    },
    444  });
    445 };
    446 types.addNullableType = function (subtype) {
    447  subtype = types.getType(subtype);
    448  return types.addType("nullable:" + subtype.name, {
    449    category: "nullable",
    450    read: (value, ctx) => {
    451      if (value == null) {
    452        return value;
    453      }
    454      return subtype.read(value, ctx);
    455    },
    456    write: (value, ctx) => {
    457      if (value == null) {
    458        return value;
    459      }
    460      return subtype.write(value, ctx);
    461    },
    462  });
    463 };
    464 
    465 /**
    466 * Register an actor detail type.  This is just like an actor type, but
    467 * will pass a detail hint to the actor's form method during serialization/
    468 * deserialization.
    469 *
    470 * This is called by getType() when passed an 'actorType#detail' string.
    471 *
    472 * @param string name
    473 *   The typestring to register this type as.
    474 * @param type actorType
    475 *   The actor type you'll be detailing.
    476 * @param string detail
    477 *   The detail to pass.
    478 */
    479 types.addActorDetail = function (name, actorType, detail) {
    480  actorType = types.getType(actorType);
    481  if (!actorType._actor) {
    482    throw Error(
    483      `Details only apply to actor types, tried to add detail '${detail}' ` +
    484        `to ${actorType.name}`
    485    );
    486  }
    487  return types.addType(name, {
    488    _actor: true,
    489    category: "detail",
    490    read: (v, ctx) => actorType.read(v, ctx, detail),
    491    write: (v, ctx) => actorType.write(v, ctx, detail),
    492  });
    493 };
    494 
    495 // Add a few named primitive types.
    496 types.Primitive = types.addType("primitive");
    497 types.String = types.addType("string");
    498 types.Number = types.addType("number");
    499 types.Boolean = types.addType("boolean");
    500 types.JSON = types.addType("json");
    501 
    502 exports.registerFront = function (cls) {
    503  const { typeName } = cls.prototype;
    504  if (!registeredTypes.has(typeName)) {
    505    types.addActorType(typeName);
    506  }
    507  registeredTypes.get(typeName).frontClass = cls;
    508 };
    509 
    510 /**
    511 * Instantiate a front of the given type.
    512 *
    513 * @param DevToolsClient client
    514 *    The DevToolsClient instance to use.
    515 * @param string typeName
    516 *    The type name of the front to instantiate. This is defined in its specifiation.
    517 * @returns Front
    518 *    The created front.
    519 */
    520 function createFront(client, typeName, target = null) {
    521  const type = types.getType(typeName);
    522  if (!type) {
    523    throw new Error(`No spec for front type '${typeName}'.`);
    524  } else if (!type.frontClass) {
    525    lazyLoadFront(typeName);
    526  }
    527 
    528  // Use intermediate Class variable to please eslint requiring
    529  // a capital letter for all constructors.
    530  const Class = type.frontClass;
    531  return new Class(client, target, target);
    532 }
    533 
    534 /**
    535 * Instantiate a global (preference, device) or target-scoped (webconsole, inspector)
    536 * front of the given type by picking its actor ID out of either the target or root
    537 * front's form.
    538 *
    539 * @param DevToolsClient client
    540 *    The DevToolsClient instance to use.
    541 * @param string typeName
    542 *    The type name of the front to instantiate. This is defined in its specifiation.
    543 * @param json form
    544 *    If we want to instantiate a global actor's front, this is the root front's form,
    545 *    otherwise we are instantiating a target-scoped front from the target front's form.
    546 * @param [Target|null] target
    547 *    If we are instantiating a target-scoped front, this is a reference to the front's
    548 *    Target instance, otherwise this is null.
    549 */
    550 async function getFront(client, typeName, form, target = null) {
    551  const front = createFront(client, typeName, target);
    552  const { formAttributeName } = front;
    553  if (!formAttributeName) {
    554    throw new Error(`Can't find the form attribute name for ${typeName}`);
    555  }
    556  // Retrieve the actor ID from root or target actor's form
    557  front.actorID = form[formAttributeName];
    558  if (!front.actorID) {
    559    throw new Error(
    560      `Can't find the actor ID for ${typeName} from root or target` +
    561        ` actor's form.`
    562    );
    563  }
    564 
    565  if (!target) {
    566    await front.manage(front);
    567  } else {
    568    await target.manage(front);
    569  }
    570 
    571  return front;
    572 }
    573 exports.getFront = getFront;
    574 
    575 /**
    576 * Create a RootFront.
    577 *
    578 * @param DevToolsClient client
    579 *    The DevToolsClient instance to use.
    580 * @param Object packet
    581 * @returns RootFront
    582 */
    583 function createRootFront(client, packet) {
    584  const rootFront = createFront(client, "root");
    585  rootFront.form(packet);
    586 
    587  // Root Front is a special case, managing itself as it doesn't have any parent.
    588  // It will register itself to DevToolsClient as a Pool via Front._poolMap.
    589  rootFront.manage(rootFront);
    590 
    591  return rootFront;
    592 }
    593 exports.createRootFront = createRootFront;