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 });