tor-browser

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

browser_jsat_serialize.js (14532B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 /* global JSActorTypeUtils */
      7 
      8 function equivArrays(src, dst, m) {
      9  ok(Array.isArray(src), "src array isArray");
     10  ok(Array.isArray(dst), "dst array isArray");
     11  ok(dst instanceof Array, "dst array is an instance of Array");
     12  is(src.length, dst.length, m + ": arrays need same length");
     13  for (let i = 0; i < src.length; i++) {
     14    if (Array.isArray(src[i])) {
     15      equivArrays(src[i], dst[i], m);
     16    } else {
     17      is(src[i], dst[i], m + ": element " + i + " should match");
     18    }
     19  }
     20 }
     21 
     22 add_task(async () => {
     23  function testPrimitive(v1) {
     24    let v2 = JSActorTypeUtils.serializeDeserialize(true, v1);
     25    is(v1, v2, "initial and deserialized values are the same");
     26  }
     27 
     28  // Undefined.
     29  testPrimitive(undefined);
     30 
     31  // String.
     32  testPrimitive("a string");
     33  testPrimitive("");
     34 
     35  // Null.
     36  testPrimitive(null);
     37 
     38  // Boolean.
     39  testPrimitive(true);
     40  testPrimitive(false);
     41 
     42  // Double.
     43  testPrimitive(3.14159);
     44  testPrimitive(-1.1);
     45  let nan2 = JSActorTypeUtils.serializeDeserialize(true, NaN);
     46  ok(Number.isNaN(nan2), "NaN deserialization works");
     47  testPrimitive(Infinity);
     48  testPrimitive(-Infinity);
     49 
     50  // int32.
     51  testPrimitive(0);
     52  testPrimitive(10001);
     53  testPrimitive(-94892);
     54  testPrimitive(2147483647);
     55  testPrimitive(-2147483648);
     56 
     57  // nsIPrincipal
     58  var sp = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
     59  testPrimitive(sp);
     60 
     61  // BrowsingContext
     62  let bc1;
     63  let TEST_URL = "https://example.org/document-builder.sjs?html=empty-document";
     64  await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
     65    bc1 = browser.browsingContext;
     66    ok(bc1, "found a BC in new tab");
     67    ok(!bc1.isDiscarded, "BC isn't discarded before we close the tab");
     68    testPrimitive(bc1);
     69  });
     70  ok(bc1.isDiscarded, "BC is discarded after we close the tab");
     71  is(
     72    JSActorTypeUtils.serializeDeserialize(true, bc1),
     73    null,
     74    "discarded BC should serialize to null"
     75  );
     76 
     77  // DOMRect.
     78  let r1 = new DOMRect(1.5, -2.8, 1e10, 0);
     79  let r2 = JSActorTypeUtils.serializeDeserialize(true, r1);
     80  ok(DOMRect.isInstance(r2));
     81  is(r1.x, r2.x, "DOMRect x");
     82  is(r1.y, r2.y, "DOMRect y");
     83  is(r1.width, r2.width, "DOMRect width");
     84  is(r1.height, r2.height, "DOMRect height");
     85 
     86  // Objects.
     87  let o1 = { a: true, 4: "int", b: 123 };
     88  let o2 = JSActorTypeUtils.serializeDeserialize(true, o1);
     89  equivArrays(Object.keys(o1), ["4", "a", "b"], "sorted keys, before");
     90  equivArrays(Object.keys(o2), ["4", "a", "b"], "sorted keys, after");
     91  is(o1.a, o2.a, "sorted keys, first property");
     92  is(o1[4], o2[4], "sorted keys, second property");
     93  is(o1.b, o2.b, "sorted keys, third property");
     94 
     95  // If an object's property is a getter, then the serialized version will have
     96  // that property as a plain data property.
     97  o1 = {
     98    get a() {
     99      return 0;
    100    },
    101  };
    102  o2 = JSActorTypeUtils.serializeDeserialize(true, o1);
    103  equivArrays(Object.keys(o2), ["a"], "getter keys, after");
    104  is(o1.a, o2.a, "value of getter matches");
    105  is(
    106    typeof Object.getOwnPropertyDescriptor(o1, "a").get,
    107    "function",
    108    "getter is a function"
    109  );
    110  let desc2 = Object.getOwnPropertyDescriptor(o2, "a");
    111  is(desc2.get, undefined, "getter turned into a plain data property");
    112  is(desc2.value, o1.a, "new data property has the correct value");
    113 
    114  // Object serialization should preserve the order of properties, because this
    115  // is visible to JS, and some code depends on it, like the receiver of
    116  // DevToolsProcessChild:packet messages.
    117  o1 = { b: "string", a: null };
    118  o2 = JSActorTypeUtils.serializeDeserialize(true, o1);
    119  equivArrays(Object.keys(o1), ["b", "a"], "unsorted keys, before");
    120  equivArrays(Object.keys(o2), ["b", "a"], "unsorted keys, after");
    121  is(o1.a, o2.a, "unsorted keys, first property");
    122  is(o1.b, o2.b, "unsorted keys, second property");
    123 
    124  // Array.
    125  let emptyArray = JSActorTypeUtils.serializeDeserialize(true, []);
    126  ok(emptyArray instanceof Array, "empty array is an array");
    127  is(emptyArray.length, 0, "empty array is empty");
    128 
    129  let array1 = [1, "hello", [true, -3.14159], undefined];
    130  let array2 = JSActorTypeUtils.serializeDeserialize(true, array1);
    131  equivArrays(array1, array2, "array before and after");
    132 
    133  // Don't preserve weird prototypes for arrays.
    134  Object.setPrototypeOf(array1, {});
    135  ok(!(array1 instanceof Array), "array1 has a non-Array prototype");
    136  array2 = JSActorTypeUtils.serializeDeserialize(true, array1);
    137  equivArrays(array1, array2, "array before and after");
    138 
    139  // An array with a hole in it gets serialized into an array without any
    140  // holes, but with undefined at the hole indexes.
    141  array1 = [1, 2, 3, 4, 5];
    142  delete array1[1];
    143  array2 = JSActorTypeUtils.serializeDeserialize(true, array1);
    144  ok(!(1 in array1), "array1 has a hole at 1");
    145  ok(1 in array2, "array2 does not have a hole at 1");
    146  is(array2[1], undefined);
    147  equivArrays(array1, array2, "array with hole before and after");
    148 
    149  // An array with a non-indexed property will not have it copied over.
    150  array1 = [1, 2, 3];
    151  array1.whatever = "whatever";
    152  array2 = JSActorTypeUtils.serializeDeserialize(true, array1);
    153  ok("whatever" in array1, "array1 has a non-indexed property");
    154  ok(!("whatever" in array2), "array2 does not have a non-indexed property");
    155  equivArrays(
    156    array1,
    157    array2,
    158    "array with non-indexed property before and after"
    159  );
    160 
    161  // Set.
    162  let emptySet = JSActorTypeUtils.serializeDeserialize(true, new Set([]));
    163  ok(emptySet instanceof Set, "empty set is a set");
    164  is(emptySet.size, 0, "empty set is empty");
    165 
    166  let set1 = new Set([1, "hello", new Set([true])]);
    167  let set2 = JSActorTypeUtils.serializeDeserialize(true, set1);
    168  ok(set2 instanceof Set, "set2 is a set");
    169  is(set2.size, 3, "set2 has correct size");
    170  ok(set2.has(1), "1 is in the set");
    171  ok(set2.has("hello"), "string is in the set");
    172  let setCount = 0;
    173  for (let e of set2) {
    174    if (setCount == 0) {
    175      is(e, 1, "first element is 1");
    176    } else if (setCount == 1) {
    177      is(e, "hello", "second element is the right string");
    178    } else if (setCount == 2) {
    179      ok(e instanceof Set, "third set element is a set");
    180      is(e.size, 1, "inner set has correct size");
    181      ok(e.has(true), "inner set contains true");
    182    } else {
    183      ok(false, "too many set elements");
    184    }
    185    setCount += 1;
    186  }
    187  is(setCount, 3, "found all set elements");
    188 
    189  // Map.
    190  let emptyMap = JSActorTypeUtils.serializeDeserialize(true, new Map([]));
    191  ok(emptyMap instanceof Map, "empty map is a map");
    192  is(emptyMap.size, 0, "empty map is empty");
    193 
    194  let map1 = new Map([
    195    [2, new Set([true])],
    196    [1, "hello"],
    197    ["bye", -11],
    198  ]);
    199  let map2 = JSActorTypeUtils.serializeDeserialize(true, map1);
    200  ok(map2 instanceof Map, "map2 is a map");
    201  is(map2.size, 3, "map has correct size");
    202  ok(map2.has(1), "1 is in the map");
    203  ok(map2.has(2), "2 is in the map");
    204  ok(map2.has("bye"), "string is in the map");
    205  let mapCount = 0;
    206  for (let e of map2) {
    207    if (mapCount == 0) {
    208      is(e[0], 2, "first key is 2");
    209      ok(e[1] instanceof Set, "first value is a set");
    210      is(e[1].size, 1, "set value has the correct size");
    211      ok(e[1].has(true), "set value contains true");
    212    } else if (mapCount == 1) {
    213      is(e[0], 1, "second key is 1");
    214      is(e[1], "hello", "second value is the right string");
    215    } else if (mapCount == 2) {
    216      is(e[0], "bye", "third key is the right string");
    217      is(e[1], -11, "third value is the right int");
    218    } else {
    219      ok(false, "too many map elements");
    220    }
    221    mapCount += 1;
    222  }
    223  is(mapCount, 3, "found all map elements");
    224 
    225  // Test that JS values that require the use of JSIPCValue's structured clone
    226  // fallback are serialized and deserialized properly.
    227  await SpecialPowers.pushPrefEnv({
    228    set: [["dom.testing.structuredclonetester.enabled", true]],
    229  });
    230  let sct1 = new StructuredCloneTester(true, true);
    231  let sct2 = JSActorTypeUtils.serializeDeserialize(true, sct1);
    232  ok(StructuredCloneTester.isInstance(sct2));
    233  is(sct1.serializable, sct2.serializable, "SC serializable");
    234  is(sct1.deserializable, sct2.deserializable, "SC serializable");
    235 
    236  // Cyclic data structures can't be serialized.
    237  let infiniteArray = [];
    238  infiniteArray[0] = infiniteArray;
    239  try {
    240    JSActorTypeUtils.serializeDeserialize(true, infiniteArray);
    241    ok(false, "serialization should have failed");
    242  } catch (e) {
    243    is(e.name, "InternalError", "expected name");
    244    is(e.message, "too much recursion", "expected message");
    245  }
    246 
    247  // Serialization doesn't preserve DAGs.
    248  let someObj = { num: -1 };
    249  let dag1 = { x: someObj, y: someObj };
    250  let dag2 = JSActorTypeUtils.serializeDeserialize(true, dag1);
    251  is(dag1.x, dag1.y, "shared object");
    252  isnot(dag2.x, dag2.y, "serialization doesn't preserve object DAGs");
    253  is(dag2.x.num, dag2.y.num, "values are copied");
    254 
    255  array1 = [3];
    256  let r = JSActorTypeUtils.serializeDeserialize(true, [array1, array1]);
    257  isnot(r[0], r[1], "serialization doesn't preserve array DAGs");
    258  equivArrays(r[0], r[1], "DAG array values are copied");
    259 });
    260 
    261 add_task(async () => {
    262  // Test the behavior of attempting to serialize a JS value that has a
    263  // component that can't be serialized. This will also demonstrate some
    264  // deliberate incompatibilities with nsFrameMessageManager::GetParamsForMessage().
    265  // In GetParamsForMessage(), if structured cloning a JS value v fails,
    266  // it instead attempts to structured clone JSON.parse(JSON.stringify(v)),
    267  // which can result in some odd behavior.
    268 
    269  function assertThrows(f, expected, desc) {
    270    let didThrow = false;
    271    try {
    272      f();
    273    } catch (e) {
    274      didThrow = true;
    275      let error = e.toString();
    276      let errorIncluded = error.includes(expected);
    277      ok(errorIncluded, desc + " exception didn't contain expected string");
    278      if (!errorIncluded) {
    279        info(`actual error: ${error}\n`);
    280      }
    281    }
    282    ok(didThrow, desc + " should throw an exception.");
    283  }
    284 
    285  function assertStrictSerializationFails(v) {
    286    assertThrows(
    287      () => JSActorTypeUtils.serializeDeserialize(true, v),
    288      "structured clone failed for strict serialization",
    289      "Strict serialization"
    290    );
    291  }
    292 
    293  function assertStructuredCloneFails(v) {
    294    assertThrows(
    295      () => structuredClone(v),
    296      "could not be cloned",
    297      "Structured clone"
    298    );
    299  }
    300 
    301  // nsFrameMessageManager::GetParamsForMessage() takes values that can't be
    302  // structured cloned and turns them into a string via JSON.stringify(), then
    303  // turns them back into a value via JSON.parse(), then attempts to structured
    304  // clone that value. This test function emulates that behavior.
    305  function getParamsForMessage(v) {
    306    try {
    307      return structuredClone(v);
    308    } catch (e) {
    309      let vString = JSON.stringify(v);
    310      if (vString == undefined) {
    311        throw new Error("not valid JSON");
    312      }
    313      return structuredClone(JSON.parse(vString));
    314    }
    315  }
    316 
    317  function assertGetParamsForMessageThrows(v) {
    318    assertThrows(
    319      () => getParamsForMessage(v),
    320      "not valid JSON",
    321      "JSON serialize"
    322    );
    323  }
    324 
    325  // Functions are neither serializable nor valid JSON.
    326  let nonSerializable = () => true;
    327 
    328  // A. Top level non-serializable value.
    329  assertStrictSerializationFails(nonSerializable);
    330  is(
    331    JSActorTypeUtils.serializeDeserialize(false, nonSerializable),
    332    undefined,
    333    "non-serializable value turns into undefined"
    334  );
    335  assertStructuredCloneFails(nonSerializable);
    336  assertGetParamsForMessageThrows(nonSerializable);
    337 
    338  // B. Arrays.
    339  // Undefined and NaN are serializable, but not valid JSON.
    340  // In an array, both are turned into null by JSON.stringify().
    341 
    342  // An array consisting entirely of serializable elements is serialized
    343  // without any changes by either method, even if it contains undefined
    344  // and NaN.
    345  let array1 = [undefined, NaN, -1];
    346  equivArrays(
    347    array1,
    348    JSActorTypeUtils.serializeDeserialize(true, array1),
    349    "array with non-JSON"
    350  );
    351  equivArrays(array1, getParamsForMessage(array1));
    352 
    353  // If we add a new non-serializable element, undefined and Nan become null
    354  // when serialized via GetParamsForMessage(). The unserializable element
    355  // becomes undefined with the typed serializer and undefined with
    356  // GetParamsForMessage().
    357  let array2 = [undefined, NaN, -1, nonSerializable];
    358  assertStrictSerializationFails(array2);
    359  equivArrays(
    360    [undefined, NaN, -1, undefined],
    361    JSActorTypeUtils.serializeDeserialize(false, array2),
    362    "array with both non-JSON and non-serializable"
    363  );
    364  equivArrays([null, null, -1, null], getParamsForMessage(array2));
    365 
    366  // C. Objects.
    367  // An object with only serializable property values is serialized without any
    368  // changes by either method, even if some property values are undefined or NaN.
    369  let obj1a = { x: undefined, y: NaN };
    370 
    371  let obj1b = JSActorTypeUtils.serializeDeserialize(true, obj1a);
    372  equivArrays(
    373    Object.keys(obj1b),
    374    ["x", "y"],
    375    "keys after typed serialization, only serializable"
    376  );
    377  is(obj1b.x, undefined, "undefined value preserved");
    378  ok(Number.isNaN(obj1b.y), "NaN value preserved");
    379 
    380  let obj1c = getParamsForMessage(obj1a);
    381  equivArrays(
    382    Object.keys(obj1c),
    383    ["x", "y"],
    384    "keys after getParamsForMessage, only serializable"
    385  );
    386  is(obj1c.x, undefined, "undefined value preserved");
    387  ok(Number.isNaN(obj1c.y), "NaN value preserved");
    388 
    389  // Now we add a property with a non-serializable value.
    390  let obj2a = { x: undefined, y: NaN, z: nonSerializable };
    391 
    392  // With typed serialization, the property with a non-serializable value gets
    393  // dropped, but everything else is preserved.
    394  assertStrictSerializationFails(obj2a);
    395  let obj2b = JSActorTypeUtils.serializeDeserialize(false, obj2a);
    396  equivArrays(
    397    Object.keys(obj2b),
    398    ["x", "y"],
    399    "keys after typed serialization, with non-serializable"
    400  );
    401  is(obj2b.x, undefined, "undefined value preserved");
    402  ok(Number.isNaN(obj2b.y), "NaN value preserved");
    403 
    404  // With GetParamsForMessage(), the property with a non-serializable value
    405  // gets dropped. However, due to the behavior of JSON.stringify(), the
    406  // property with the value undefined is also dropped, while the property
    407  // with the value NaN is kept, but the value is changed to null.
    408  let obj2c = getParamsForMessage(obj2a);
    409  equivArrays(
    410    Object.keys(obj2c),
    411    ["y"],
    412    "keys after getParamsForMessage, with non-serializable"
    413  );
    414  is(obj2c.y, null, "NaN property value turned to null");
    415 });