Bug 1036136 - Implement structured cloning for Map and Set objects. r=jorendorff,bent
authorTom Schuster <evilpies@gmail.com>
Sat, 19 Jul 2014 23:44:53 +0200
changeset 217105 225fa7edfb16880d1f687eacea2d7bb41d75b553
parent 217104 88a6043b07e01b87e94f95e60563174047c91227
child 217106 289695fac6be2883c2267f21273c9ee54642a2d4
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff, bent
bugs1036136
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1036136 - Implement structured cloning for Map and Set objects. r=jorendorff,bent
dom/indexedDB/OpenDatabaseHelper.cpp
js/public/StructuredClone.h
js/src/builtin/MapObject.cpp
js/src/builtin/MapObject.h
js/src/jit-test/tests/structured-clone/Map.js
js/src/jit-test/tests/structured-clone/Set.js
js/src/jit-test/tests/structured-clone/version3.js
js/src/vm/StructuredClone.cpp
--- a/dom/indexedDB/OpenDatabaseHelper.cpp
+++ b/dom/indexedDB/OpenDatabaseHelper.cpp
@@ -30,21 +30,21 @@ using namespace mozilla;
 using namespace mozilla::dom;
 USING_INDEXEDDB_NAMESPACE
 USING_QUOTA_NAMESPACE
 
 namespace {
 
 // If JS_STRUCTURED_CLONE_VERSION changes then we need to update our major
 // schema version.
-static_assert(JS_STRUCTURED_CLONE_VERSION == 3,
+static_assert(JS_STRUCTURED_CLONE_VERSION == 4,
               "Need to update the major schema version.");
 
 // Major schema version. Bump for almost everything.
-const uint32_t kMajorSchemaVersion = 15;
+const uint32_t kMajorSchemaVersion = 16;
 
 // Minor schema version. Should almost always be 0 (maybe bump on release
 // branches if we have to).
 const uint32_t kMinorSchemaVersion = 0;
 
 // The schema version we store in the SQLite database is a (signed) 32-bit
 // integer. The major version is left-shifted 4 bits so the max value is
 // 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF.
@@ -1466,16 +1466,27 @@ UpgradeSchemaFrom14_0To15_0(mozIStorageC
   // The only change between 14 and 15 was a different structured
   // clone format, but it's backwards-compatible.
   nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(15, 0));
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
+nsresult
+UpgradeSchemaFrom15_0To16_0(mozIStorageConnection* aConnection)
+{
+  // The only change between 15 and 16 was a different structured
+  // clone format, but it's backwards-compatible.
+  nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(16, 0));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
 class VersionChangeEventsRunnable;
 
 class SetVersionHelper : public AsyncConnectionHelper,
                          public IDBTransactionListener,
                          public AcquireListener
 {
   friend class VersionChangeEventsRunnable;
 
@@ -2083,17 +2094,17 @@ OpenDatabaseHelper::CreateDatabaseConnec
       rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), aName);
       IDB_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
       rv = stmt->Execute();
       IDB_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
     }
     else  {
       // This logic needs to change next time we change the schema!
-      static_assert(kSQLiteSchemaVersion == int32_t((15 << 4) + 0),
+      static_assert(kSQLiteSchemaVersion == int32_t((16 << 4) + 0),
                     "Need upgrade code from schema version increase.");
 
       while (schemaVersion != kSQLiteSchemaVersion) {
         if (schemaVersion == 4) {
           rv = UpgradeSchemaFrom4To5(connection);
         }
         else if (schemaVersion == 5) {
           rv = UpgradeSchemaFrom5To6(connection);
@@ -2121,16 +2132,19 @@ OpenDatabaseHelper::CreateDatabaseConnec
           rv = UpgradeSchemaFrom12_0To13_0(connection, &vacuumNeeded);
         }
         else if (schemaVersion == MakeSchemaVersion(13, 0)) {
           rv = UpgradeSchemaFrom13_0To14_0(connection);
         }
         else if (schemaVersion == MakeSchemaVersion(14, 0)) {
           rv = UpgradeSchemaFrom14_0To15_0(connection);
         }
+        else if (schemaVersion == MakeSchemaVersion(15, 0)) {
+          rv = UpgradeSchemaFrom15_0To16_0(connection);
+        }
         else {
           NS_WARNING("Unable to open IndexedDB database, no upgrade path is "
                      "available!");
           IDB_REPORT_INTERNAL_ERR();
           return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
         }
         NS_ENSURE_SUCCESS(rv, rv);
 
--- a/js/public/StructuredClone.h
+++ b/js/public/StructuredClone.h
@@ -116,17 +116,17 @@ typedef bool (*TransferStructuredCloneOp
 // object. Note that it should never trigger a garbage collection (and will
 // assert in a debug build if it does.)
 typedef void (*FreeTransferStructuredCloneOp)(uint32_t tag, JS::TransferableOwnership ownership,
                                               void *content, uint64_t extraData, void *closure);
 
 // The maximum supported structured-clone serialization format version. Note
 // that this does not need to be bumped for Transferable-only changes, since
 // they are never saved to persistent storage.
-#define JS_STRUCTURED_CLONE_VERSION 3
+#define JS_STRUCTURED_CLONE_VERSION 4
 
 struct JSStructuredCloneCallbacks {
     ReadStructuredCloneOp read;
     WriteStructuredCloneOp write;
     StructuredCloneErrorOp reportError;
     ReadTransferStructuredCloneOp readTransfer;
     TransferStructuredCloneOp writeTransfer;
     FreeTransferStructuredCloneOp freeTransfer;
--- a/js/src/builtin/MapObject.cpp
+++ b/js/src/builtin/MapObject.cpp
@@ -1151,45 +1151,94 @@ WriteBarrierPost(JSRuntime *rt, ValueSet
     typedef OrderedHashSet<Value, UnbarrieredHashPolicy, RuntimeAllocPolicy> UnbarrieredSet;
     if (MOZ_UNLIKELY(key.isObject() && IsInsideNursery(&key.toObject()))) {
         rt->gc.storeBuffer.putGeneric(OrderedHashTableRef<UnbarrieredSet>(
                     reinterpret_cast<UnbarrieredSet *>(set), key));
     }
 #endif
 }
 
+bool
+MapObject::entries(JSContext *cx, HandleObject obj, JS::AutoValueVector *entries)
+{
+    ValueMap *map = obj->as<MapObject>().getData();
+    if (!map)
+        return false;
+
+    for (ValueMap::Range r = map->all(); !r.empty(); r.popFront()) {
+        if (!entries->append(r.front().key.get()) ||
+            !entries->append(r.front().value))
+        {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool
+MapObject::set(JSContext *cx, HandleObject obj, HandleValue k, HandleValue v)
+{
+    ValueMap *map = obj->as<MapObject>().getData();
+    if (!map)
+        return false;
+
+    AutoHashableValueRooter key(cx);
+    if (!key.setValue(cx, k))
+        return false;
+
+    RelocatableValue rval(v);
+    if (!map->put(key, rval)) {
+        js_ReportOutOfMemory(cx);
+        return false;
+    }
+    WriteBarrierPost(cx->runtime(), map, key.get());
+    return true;
+}
+
+MapObject*
+MapObject::create(JSContext *cx)
+{
+    RootedObject obj(cx, NewBuiltinClassInstance(cx, &class_));
+    if (!obj)
+        return nullptr;
+
+    ValueMap *map = cx->new_<ValueMap>(cx->runtime());
+    if (!map || !map->init()) {
+        js_delete(map);
+        js_ReportOutOfMemory(cx);
+        return nullptr;
+    }
+
+    obj->setPrivate(map);
+    return &obj->as<MapObject>();
+}
+
 void
 MapObject::finalize(FreeOp *fop, JSObject *obj)
 {
     if (ValueMap *map = obj->as<MapObject>().getData())
         fop->delete_(map);
 }
 
 bool
 MapObject::construct(JSContext *cx, unsigned argc, Value *vp)
 {
-    Rooted<JSObject*> obj(cx, NewBuiltinClassInstance(cx, &class_));
+    Rooted<MapObject*> obj(cx, MapObject::create(cx));
     if (!obj)
         return false;
 
-    ValueMap *map = cx->new_<ValueMap>(cx->runtime());
-    if (!map || !map->init()) {
-        js_delete(map);
-        js_ReportOutOfMemory(cx);
-        return false;
-    }
-    obj->setPrivate(map);
-
     CallArgs args = CallArgsFromVp(argc, vp);
     if (args.hasDefined(0)) {
         ForOfIterator iter(cx);
         if (!iter.init(args[0]))
             return false;
         RootedValue pairVal(cx);
         RootedObject pairObj(cx);
+        ValueMap *map = obj->getData();
         while (true) {
             bool done;
             if (!iter.next(&pairVal, &done))
                 return false;
             if (done)
                 break;
             if (!pairVal.isObject()) {
                 JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_INVALID_MAP_ITERABLE);
@@ -1649,16 +1698,68 @@ SetObject::initClass(JSContext *cx, JSOb
         if (!JS_DefineProperty(cx, proto, "keys", funval, 0))
             return nullptr;
         if (!JS_DefineProperty(cx, proto, js_std_iterator_str, funval, 0))
             return nullptr;
     }
     return proto;
 }
 
+
+bool
+SetObject::keys(JSContext *cx, HandleObject obj, JS::AutoValueVector *keys)
+{
+    ValueSet *set = obj->as<SetObject>().getData();
+    if (!set)
+        return false;
+
+    for (ValueSet::Range r = set->all(); !r.empty(); r.popFront()) {
+        if (!keys->append(r.front().get()))
+            return false;
+    }
+
+    return true;
+}
+
+bool
+SetObject::add(JSContext *cx, HandleObject obj, HandleValue k)
+{
+    ValueSet *set = obj->as<SetObject>().getData();
+    if (!set)
+        return false;
+
+    AutoHashableValueRooter key(cx);
+    if (!key.setValue(cx, k))
+        return false;
+
+    if (!set->put(key)) {
+        js_ReportOutOfMemory(cx);
+        return false;
+    }
+    WriteBarrierPost(cx->runtime(), set, key.get());
+    return true;
+}
+
+SetObject*
+SetObject::create(JSContext *cx)
+{
+    RootedObject obj(cx, NewBuiltinClassInstance(cx, &class_));
+    if (!obj)
+        return nullptr;
+
+    ValueSet *set = cx->new_<ValueSet>(cx->runtime());
+    if (!set || !set->init()) {
+        js_delete(set);
+        js_ReportOutOfMemory(cx);
+        return nullptr;
+    }
+    obj->setPrivate(set);
+    return &obj->as<SetObject>();
+}
+
 void
 SetObject::mark(JSTracer *trc, JSObject *obj)
 {
     SetObject *setobj = static_cast<SetObject *>(obj);
     if (ValueSet *set = setobj->getData()) {
         for (ValueSet::Range r = set->all(); !r.empty(); r.popFront())
             MarkKey(r, r.front(), trc);
     }
@@ -1670,35 +1771,28 @@ SetObject::finalize(FreeOp *fop, JSObjec
     SetObject *setobj = static_cast<SetObject *>(obj);
     if (ValueSet *set = setobj->getData())
         fop->delete_(set);
 }
 
 bool
 SetObject::construct(JSContext *cx, unsigned argc, Value *vp)
 {
-    Rooted<JSObject*> obj(cx, NewBuiltinClassInstance(cx, &class_));
+    Rooted<SetObject*> obj(cx, SetObject::create(cx));
     if (!obj)
         return false;
 
-    ValueSet *set = cx->new_<ValueSet>(cx->runtime());
-    if (!set || !set->init()) {
-        js_delete(set);
-        js_ReportOutOfMemory(cx);
-        return false;
-    }
-    obj->setPrivate(set);
-
     CallArgs args = CallArgsFromVp(argc, vp);
     if (args.hasDefined(0)) {
         RootedValue keyVal(cx);
         ForOfIterator iter(cx);
         if (!iter.init(args[0]))
             return false;
         AutoHashableValueRooter key(cx);
+        ValueSet *set = obj->getData();
         while (true) {
             bool done;
             if (!iter.next(&keyVal, &done))
                 return false;
             if (done)
                 break;
             if (!key.setValue(cx, keyVal))
                 return false;
--- a/js/src/builtin/MapObject.h
+++ b/js/src/builtin/MapObject.h
@@ -86,16 +86,22 @@ typedef OrderedHashSet<HashableValue,
                        RuntimeAllocPolicy> ValueSet;
 
 class MapObject : public JSObject {
   public:
     enum IteratorKind { Keys, Values, Entries };
 
     static JSObject *initClass(JSContext *cx, JSObject *obj);
     static const Class class_;
+
+    // Entries is every key followed by value.
+    static bool entries(JSContext *cx, HandleObject obj, JS::AutoValueVector *entries);
+    static bool set(JSContext *cx, HandleObject obj, HandleValue key, HandleValue value);
+    static MapObject* create(JSContext *cx);
+
   private:
     static const JSPropertySpec properties[];
     static const JSFunctionSpec methods[];
     ValueMap *getData() { return static_cast<ValueMap *>(getPrivate()); }
     static ValueMap & extract(CallReceiver call);
     static void mark(JSTracer *trc, JSObject *obj);
     static void finalize(FreeOp *fop, JSObject *obj);
     static bool construct(JSContext *cx, unsigned argc, Value *vp);
@@ -124,16 +130,21 @@ class MapObject : public JSObject {
     static bool clear(JSContext *cx, unsigned argc, Value *vp);
 };
 
 class SetObject : public JSObject {
   public:
     enum IteratorKind { Values, Entries };
     static JSObject *initClass(JSContext *cx, JSObject *obj);
     static const Class class_;
+
+    static bool keys(JSContext *cx, HandleObject obj, JS::AutoValueVector *keys);
+    static bool add(JSContext *cx, HandleObject obj, HandleValue key);
+    static SetObject* create(JSContext *cx);
+
   private:
     static const JSPropertySpec properties[];
     static const JSFunctionSpec methods[];
     ValueSet *getData() { return static_cast<ValueSet *>(getPrivate()); }
     static ValueSet & extract(CallReceiver call);
     static void mark(JSTracer *trc, JSObject *obj);
     static void finalize(FreeOp *fop, JSObject *obj);
     static bool construct(JSContext *cx, unsigned argc, Value *vp);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/structured-clone/Map.js
@@ -0,0 +1,110 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ */
+
+var map = new Map();
+map.set("self", map);
+
+var magic = deserialize(serialize(map));
+assertEq(magic.get("self"), magic);
+assertEq(magic.size, 1);
+
+map = new Map();
+map.set(map, "self");
+
+magic = deserialize(serialize(map));
+assertEq(magic.get(magic), "self");
+assertEq(magic.size, 1);
+
+var values = [
+    "a", "\uDEFF", undefined, null, -3.5, true, false, NaN, 155, -2
+]
+
+map = new Map();
+for (var value of values) {
+    map.set(value, value);
+}
+
+magic = deserialize(serialize(map));
+var i = 0;
+for (value of magic) {
+    assertEq(value[0], value[1]);
+    assertEq(value[0], values[i++]);
+}
+
+assertEq([...map.keys()].toSource(), [...magic.keys()].toSource());
+assertEq([...map.values()].toSource(), [...magic.values()].toSource());
+
+var obj = {a: 1};
+obj.map = new Map();
+obj.map.set("obj", obj);
+
+magic = deserialize(serialize(obj));
+
+assertEq(magic.map.get("obj"), magic);
+assertEq(magic.a, 1);
+
+map = new Map();
+map.set("a", new Number(1));
+map.set("b", new String("aaaa"));
+map.set("c", new Date(NaN));
+
+magic = deserialize(serialize(map));
+
+assertEq(magic.get("a").valueOf(), 1);
+assertEq(magic.get("b").valueOf(), "aaaa");
+assertEq(magic.get("c").valueOf(), NaN);
+
+assertEq([...magic.keys()].toSource(), ["a", "b", "c"].toSource());
+
+map = new Map();
+map.set("x", new Map());
+map.get("x").set("x", map);
+map.get("x").set("b", null);
+
+magic = deserialize(serialize(map));
+
+assertEq(magic.get("x").get("x"), magic);
+assertEq(magic.get("x").get("b"), null);
+
+map = new Map()
+map.set({a: 1}, "b");
+
+magic = deserialize(serialize(map));
+
+obj = [...magic.keys()][0];
+assertEq(obj.a, 1);
+assertEq(magic.get(obj), "b");
+
+// Make sure expandos aren't cloned (Bug 1041172)
+map = new Map();
+map.a = "aaaaa";
+magic = deserialize(serialize(map));
+assertEq("a" in magic, false);
+assertEq(Object.keys(magic).length, 0);
+
+// Busted [[Prototype]] shouldn't matter
+map = new Map();
+Object.setPrototypeOf(map, null);
+Map.prototype.set.call(map, "self", map);
+magic = deserialize(serialize(map));
+assertEq(magic.get("self"), magic);
+assertEq(magic.size, 1);
+
+// Can't fuzz around with Map after it is cloned
+obj = {
+    a: new Map(),
+    get b() {
+        obj.a.delete("test");
+        return "invoked";
+    }
+}
+obj.a.set("test", "hello");
+assertEq(obj.a.has("test"), true);
+magic = deserialize(serialize(obj));
+assertEq(obj.a.has("test"), false);
+assertEq(magic.a.size, 1);
+assertEq(magic.a.get("test"), "hello");
+assertEq([...magic.a.keys()].toString(), "test");
+assertEq(magic.b, "invoked");
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/structured-clone/Set.js
@@ -0,0 +1,82 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ */
+
+var set = new Set();
+set.add(set);
+
+var magic = deserialize(serialize(set));
+assertEq(magic.size, 1);
+assertEq(magic.values().next().value, magic);
+
+var values = [
+    "a", "\uDEFF", undefined, null, -3.5, true, false, NaN, 155, -2
+]
+
+set = new Set();
+for (var value of values) {
+    set.add(value)
+}
+
+magic = deserialize(serialize(set));
+var i = 0;
+for (value of magic) {
+    assertEq(value, values[i++]);
+}
+
+assertEq([...set.keys()].toSource(), [...magic.keys()].toSource());
+assertEq([...set.values()].toSource(), [...magic.values()].toSource());
+
+var obj = {a: 1};
+obj.set = new Set();
+obj.set.add(obj);
+
+magic = deserialize(serialize(obj));
+
+assertEq(magic.set.values().next().value, magic);
+assertEq(magic.a, 1);
+
+set = new Set();
+set.add(new Number(1));
+set.add(new String("aaaa"));
+set.add(new Date(NaN));
+
+magic = deserialize(serialize(set));
+
+values = magic.values();
+assertEq(values.next().value.valueOf(), 1);
+assertEq(values.next().value.valueOf(), "aaaa");
+assertEq(values.next().value.valueOf(), NaN);
+assertEq(values.next().done, true);
+
+// Make sure expandos aren't cloned (Bug 1041172)
+set = new Set();
+set.a = "aaaaa";
+magic = deserialize(serialize(set));
+assertEq("a" in magic, false);
+assertEq(Object.keys(magic).length, 0);
+
+// Busted [[Prototype]] shouldn't matter
+set = new Set();
+Object.setPrototypeOf(set, null);
+Set.prototype.add.call(set, "aaa");
+magic = deserialize(serialize(set));
+assertEq(magic.has("aaa"), true);
+assertEq(magic.size, 1);
+
+// Can't fuzz around with Set after it is cloned
+obj = {
+    a: new Set(),
+    get b() {
+        obj.a.delete("test");
+        return "invoked";
+    }
+}
+obj.a.add("test");
+assertEq(obj.a.has("test"), true);
+magic = deserialize(serialize(obj));
+assertEq(obj.a.has("test"), false);
+assertEq(magic.a.size, 1);
+assertEq([...magic.a.keys()].toString(), "test");
+assertEq(magic.b, "invoked");
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/structured-clone/version3.js
@@ -0,0 +1,13 @@
+// Created with JS_STRUCTURED_CLONE_VERSION = 3
+// var x = {
+//     "ab": 1,
+//     12: 2,
+// };
+// print(uneval(serialize(x).clonebuffer));
+
+var clonebuffer = serialize("abc");
+clonebuffer.clonebuffer = "\x00\x00\x00\x00\b\x00\xFF\xFF\f\x00\x00\x00\x03\x00\xFF\xFF\x00\x00\x00\x00\x00\x00\x00@\x02\x00\x00\x00\x04\x00\xFF\xFFa\x00b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xF0?\x00\x00\x00\x00\x00\x00\xFF\xFF"
+var obj = deserialize(clonebuffer)
+assertEq(obj.ab, 1);
+assertEq(obj[12], 2);
+assertEq(Object.keys(obj).toString(), "12,ab");
--- a/js/src/vm/StructuredClone.cpp
+++ b/js/src/vm/StructuredClone.cpp
@@ -35,16 +35,17 @@
 
 #include <algorithm>
 
 #include "jsapi.h"
 #include "jscntxt.h"
 #include "jsdate.h"
 #include "jswrapper.h"
 
+#include "builtin/MapObject.h"
 #include "vm/SharedArrayObject.h"
 #include "vm/TypedArrayObject.h"
 #include "vm/WrapperObject.h"
 
 #include "jscntxtinlines.h"
 #include "jsobjinlines.h"
 
 using namespace js;
@@ -55,30 +56,34 @@ using mozilla::NativeEndian;
 using JS::CanonicalizeNaN;
 
 enum StructuredDataType MOZ_ENUM_TYPE(uint32_t) {
     /* Structured data types provided by the engine */
     SCTAG_FLOAT_MAX = 0xFFF00000,
     SCTAG_NULL = 0xFFFF0000,
     SCTAG_UNDEFINED,
     SCTAG_BOOLEAN,
-    SCTAG_INDEX,
+    SCTAG_INT32,
     SCTAG_STRING,
     SCTAG_DATE_OBJECT,
     SCTAG_REGEXP_OBJECT,
     SCTAG_ARRAY_OBJECT,
     SCTAG_OBJECT_OBJECT,
     SCTAG_ARRAY_BUFFER_OBJECT,
     SCTAG_BOOLEAN_OBJECT,
     SCTAG_STRING_OBJECT,
     SCTAG_NUMBER_OBJECT,
     SCTAG_BACK_REFERENCE_OBJECT,
     SCTAG_DO_NOT_USE_1, // Required for backwards compatibility
     SCTAG_DO_NOT_USE_2, // Required for backwards compatibility
     SCTAG_TYPED_ARRAY_OBJECT,
+    SCTAG_MAP_OBJECT,
+    SCTAG_SET_OBJECT,
+    SCTAG_END_OF_KEYS,
+
     SCTAG_TYPED_ARRAY_V1_MIN = 0xFFFF0100,
     SCTAG_TYPED_ARRAY_V1_INT8 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int8,
     SCTAG_TYPED_ARRAY_V1_UINT8 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint8,
     SCTAG_TYPED_ARRAY_V1_INT16 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int16,
     SCTAG_TYPED_ARRAY_V1_UINT16 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint16,
     SCTAG_TYPED_ARRAY_V1_INT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Int32,
     SCTAG_TYPED_ARRAY_V1_UINT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint32,
     SCTAG_TYPED_ARRAY_V1_FLOAT32 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Float32,
@@ -217,17 +222,16 @@ struct JSStructuredCloneReader {
     template <typename CharT>
     JSString *readStringImpl(uint32_t nchars);
     JSString *readString(uint32_t data);
 
     bool checkDouble(double d);
     bool readTypedArray(uint32_t arrayType, uint32_t nelems, Value *vp, bool v1Read = false);
     bool readArrayBuffer(uint32_t nbytes, Value *vp);
     bool readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems, Value *vp);
-    bool readId(jsid *idp);
     bool startRead(Value *vp);
 
     SCInput &in;
 
     // Stack of objects with properties remaining to be read.
     AutoValueVector objs;
 
     // Stack of all objects read during this deserialization
@@ -244,17 +248,17 @@ struct JSStructuredCloneReader {
 
 struct JSStructuredCloneWriter {
   public:
     explicit JSStructuredCloneWriter(JSContext *cx,
                                      const JSStructuredCloneCallbacks *cb,
                                      void *cbClosure,
                                      jsval tVal)
         : out(cx), objs(out.context()),
-          counts(out.context()), ids(out.context()),
+          counts(out.context()), entries(out.context()),
           memory(out.context()), callbacks(cb), closure(cbClosure),
           transferable(out.context(), tVal), transferableObjects(out.context()) { }
 
     ~JSStructuredCloneWriter();
 
     bool init() { return memory.init() && parseTransferable() && writeTransferMap(); }
 
     bool write(const Value &v);
@@ -266,43 +270,46 @@ struct JSStructuredCloneWriter {
     }
 
   private:
     JSContext *context() { return out.context(); }
 
     bool writeTransferMap();
 
     bool writeString(uint32_t tag, JSString *str);
-    bool writeId(jsid id);
     bool writeArrayBuffer(HandleObject obj);
     bool writeTypedArray(HandleObject obj);
     bool startObject(HandleObject obj, bool *backref);
     bool startWrite(const Value &v);
     bool traverseObject(HandleObject obj);
+    bool traverseMap(HandleObject obj);
+    bool traverseSet(HandleObject obj);
 
     bool parseTransferable();
     bool reportErrorTransferable();
     bool transferOwnership();
 
     inline void checkStack();
 
     SCOutput out;
 
     // Vector of objects with properties remaining to be written.
     //
     // NB: These can span multiple compartments, so the compartment must be
     // entered before any manipulation is performed.
     AutoValueVector objs;
 
-    // counts[i] is the number of properties of objs[i] remaining to be written.
-    // counts.length() == objs.length() and sum(counts) == ids.length().
+    // counts[i] is the number of entries of objs[i] remaining to be written.
+    // counts.length() == objs.length() and sum(counts) == entries.length().
     Vector<size_t> counts;
 
-    // Ids of properties remaining to be written.
-    AutoIdVector ids;
+    // For JSObject: Propery IDs as value
+    // For Map: Key followed by value.
+    // For Set: Key
+    AutoValueVector entries;
 
     // The "memory" list described in the HTML5 internal structured cloning algorithm.
     // memory is a superset of objs; items are never removed from Memory
     // until a serialization operation is finished
     typedef AutoObjectUnsigned32HashMap CloneMemory;
     CloneMemory memory;
 
     // The user defined callbacks that will be used for cloning.
@@ -806,43 +813,34 @@ JSStructuredCloneWriter::writeString(uin
         return false;
 
     JS::AutoCheckCannotGC nogc;
     return linear->hasLatin1Chars()
            ? out.writeChars(linear->latin1Chars(nogc), length)
            : out.writeChars(linear->twoByteChars(nogc), length);
 }
 
-bool
-JSStructuredCloneWriter::writeId(jsid id)
-{
-    if (JSID_IS_INT(id))
-        return out.writePair(SCTAG_INDEX, uint32_t(JSID_TO_INT(id)));
-    JS_ASSERT(JSID_IS_STRING(id));
-    return writeString(SCTAG_STRING, JSID_TO_STRING(id));
-}
-
 inline void
 JSStructuredCloneWriter::checkStack()
 {
 #ifdef DEBUG
     /* To avoid making serialization O(n^2), limit stack-checking at 10. */
     const size_t MAX = 10;
 
     size_t limit = Min(counts.length(), MAX);
     JS_ASSERT(objs.length() == counts.length());
     size_t total = 0;
     for (size_t i = 0; i < limit; i++) {
         JS_ASSERT(total + counts[i] >= total);
         total += counts[i];
     }
     if (counts.length() <= MAX)
-        JS_ASSERT(total == ids.length());
+        JS_ASSERT(total == entries.length());
     else
-        JS_ASSERT(total <= ids.length());
+        JS_ASSERT(total <= entries.length());
 
     size_t j = objs.length();
     for (size_t i = 0; i < limit; i++)
         JS_ASSERT(memory.has(&objs[--j].toObject()));
 #endif
 }
 
 /*
@@ -906,32 +904,81 @@ JSStructuredCloneWriter::startObject(Han
 
 bool
 JSStructuredCloneWriter::traverseObject(HandleObject obj)
 {
     /*
      * Get enumerable property ids and put them in reverse order so that they
      * will come off the stack in forward order.
      */
-    size_t initialLength = ids.length();
-    if (!GetPropertyNames(context(), obj, JSITER_OWNONLY, &ids))
+    AutoIdVector properties(context());
+    if (!GetPropertyNames(context(), obj, JSITER_OWNONLY, &properties))
         return false;
-    jsid *begin = ids.begin() + initialLength, *end = ids.end();
-    size_t count = size_t(end - begin);
-    Reverse(begin, end);
+
+    for (size_t i = properties.length(); i > 0; --i) {
+        MOZ_ASSERT(JSID_IS_STRING(properties[i - 1]) || JSID_IS_INT(properties[i - 1]));
+        RootedValue val(context(), IdToValue(properties[i - 1]));
+        if (!entries.append(val))
+            return false;
+    }
 
     /* Push obj and count to the stack. */
-    if (!objs.append(ObjectValue(*obj)) || !counts.append(count))
+    if (!objs.append(ObjectValue(*obj)) || !counts.append(properties.length()))
         return false;
+
     checkStack();
 
     /* Write the header for obj. */
     return out.writePair(obj->is<ArrayObject>() ? SCTAG_ARRAY_OBJECT : SCTAG_OBJECT_OBJECT, 0);
 }
 
+bool
+JSStructuredCloneWriter::traverseMap(HandleObject obj)
+{
+    AutoValueVector newEntries(context());
+    if (!MapObject::entries(context(), obj, &newEntries))
+        return false;
+
+    for (size_t i = newEntries.length(); i > 0; --i) {
+        if (!entries.append(newEntries[i - 1]))
+            return false;
+    }
+
+    /* Push obj and count to the stack. */
+    if (!objs.append(ObjectValue(*obj)) || !counts.append(newEntries.length()))
+        return false;
+
+    checkStack();
+
+    /* Write the header for obj. */
+    return out.writePair(SCTAG_MAP_OBJECT, 0);
+}
+
+bool
+JSStructuredCloneWriter::traverseSet(HandleObject obj)
+{
+    AutoValueVector keys(context());
+    if (!SetObject::keys(context(), obj, &keys))
+        return false;
+
+    for (size_t i = keys.length(); i > 0; --i) {
+        if (!entries.append(keys[i - 1]))
+            return false;
+    }
+
+    /* Push obj and count to the stack. */
+    if (!objs.append(ObjectValue(*obj)) || !counts.append(keys.length()))
+        return false;
+
+    checkStack();
+
+    /* Write the header for obj. */
+    return out.writePair(SCTAG_SET_OBJECT, 0);
+}
+
 static bool
 PrimitiveToObject(JSContext *cx, Value *vp)
 {
     JSObject *obj = PrimitiveToObject(cx, *vp);
     if (!obj)
         return false;
 
     vp->setObject(*obj);
@@ -940,18 +987,20 @@ PrimitiveToObject(JSContext *cx, Value *
 
 bool
 JSStructuredCloneWriter::startWrite(const Value &v)
 {
     assertSameCompartment(context(), v);
 
     if (v.isString()) {
         return writeString(SCTAG_STRING, v.toString());
-    } else if (v.isNumber()) {
-        return out.writeDouble(v.toNumber());
+    } else if (v.isInt32()) {
+        return out.writePair(SCTAG_INT32, v.toInt32());
+    } else if (v.isDouble()) {
+        return out.writeDouble(v.toDouble());
     } else if (v.isBoolean()) {
         return out.writePair(SCTAG_BOOLEAN, v.toBoolean());
     } else if (v.isNull()) {
         return out.writePair(SCTAG_NULL, 0);
     } else if (v.isUndefined()) {
         return out.writePair(SCTAG_UNDEFINED, 0);
     } else if (v.isObject()) {
         RootedObject obj(context(), &v.toObject());
@@ -987,16 +1036,20 @@ JSStructuredCloneWriter::startWrite(cons
             return traverseObject(obj);
         } else if (obj->is<BooleanObject>()) {
             return out.writePair(SCTAG_BOOLEAN_OBJECT, obj->as<BooleanObject>().unbox());
         } else if (obj->is<NumberObject>()) {
             return out.writePair(SCTAG_NUMBER_OBJECT, 0) &&
                    out.writeDouble(obj->as<NumberObject>().unbox());
         } else if (obj->is<StringObject>()) {
             return writeString(SCTAG_STRING_OBJECT, obj->as<StringObject>().unbox());
+        } else if (obj->is<MapObject>()) {
+            return traverseMap(obj);
+        } else if (obj->is<SetObject>()) {
+            return traverseSet(obj);
         }
 
         if (callbacks && callbacks->write)
             return callbacks->write(context(), this, obj, closure);
         /* else fall through */
     }
 
     JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr, JSMSG_SC_UNSUPPORTED_TYPE);
@@ -1111,39 +1164,58 @@ JSStructuredCloneWriter::write(const Val
     if (!startWrite(v))
         return false;
 
     while (!counts.empty()) {
         RootedObject obj(context(), &objs.back().toObject());
         AutoCompartment ac(context(), obj);
         if (counts.back()) {
             counts.back()--;
-            RootedId id(context(), ids.back());
-            ids.popBack();
+            RootedValue key(context(), entries.back());
+            entries.popBack();
             checkStack();
-            if (JSID_IS_STRING(id) || JSID_IS_INT(id)) {
+
+            if (obj->is<MapObject>()) {
+                counts.back()--;
+                RootedValue val(context(), entries.back());
+                entries.popBack();
+                checkStack();
+
+                if (!startWrite(key) || !startWrite(val))
+                    return false;
+            } else if (obj->is<SetObject>()) {
+                if (!startWrite(key))
+                    return false;
+            } else {
+                RootedId id(context());
+                if (!ValueToId<CanGC>(context(), key, &id))
+                  return false;
+                MOZ_ASSERT(JSID_IS_STRING(id) || JSID_IS_INT(id));
+
                 /*
                  * If obj still has an own property named id, write it out.
                  * The cost of re-checking could be avoided by using
                  * NativeIterators.
                  */
                 bool found;
                 if (!HasOwnProperty(context(), obj, id, &found))
                     return false;
 
                 if (found) {
                     RootedValue val(context());
-                    if (!writeId(id) ||
+                    if (!startWrite(key) ||
                         !JSObject::getGeneric(context(), obj, obj, id, &val) ||
                         !startWrite(val))
+                    {
                         return false;
+                    }
                 }
             }
         } else {
-            out.writePair(SCTAG_NULL, 0);
+            out.writePair(SCTAG_END_OF_KEYS, 0);
             objs.popBack();
             counts.popBack();
         }
     }
 
     memory.clear();
     return transferOwnership();
 }
@@ -1354,16 +1426,20 @@ JSStructuredCloneReader::startRead(Value
       case SCTAG_NULL:
         vp->setNull();
         break;
 
       case SCTAG_UNDEFINED:
         vp->setUndefined();
         break;
 
+      case SCTAG_INT32:
+        vp->setInt32(data);
+        break;
+
       case SCTAG_BOOLEAN:
       case SCTAG_BOOLEAN_OBJECT:
         vp->setBoolean(!!data);
         if (tag == SCTAG_BOOLEAN_OBJECT && !PrimitiveToObject(context(), vp))
             return false;
         break;
 
       case SCTAG_STRING:
@@ -1466,16 +1542,32 @@ JSStructuredCloneReader::startRead(Value
 
       case SCTAG_TYPED_ARRAY_OBJECT:
         // readTypedArray adds the array to allObjs
         uint64_t arrayType;
         if (!in.read(&arrayType))
             return false;
         return readTypedArray(arrayType, data, vp);
 
+      case SCTAG_MAP_OBJECT: {
+        JSObject *obj = MapObject::create(context());
+        if (!obj || !objs.append(ObjectValue(*obj)))
+            return false;
+        vp->setObject(*obj);
+        break;
+      }
+
+      case SCTAG_SET_OBJECT: {
+        JSObject *obj = SetObject::create(context());
+        if (!obj || !objs.append(ObjectValue(*obj)))
+            return false;
+        vp->setObject(*obj);
+        break;
+      }
+
       default: {
         if (tag <= SCTAG_FLOAT_MAX) {
             double d = ReinterpretPairAsDouble(tag, data);
             if (!checkDouble(d))
                 return false;
             vp->setNumber(d);
             break;
         }
@@ -1500,46 +1592,16 @@ JSStructuredCloneReader::startRead(Value
 
     if (vp->isObject() && !allObjs.append(*vp))
         return false;
 
     return true;
 }
 
 bool
-JSStructuredCloneReader::readId(jsid *idp)
-{
-    uint32_t tag, data;
-    if (!in.readPair(&tag, &data))
-        return false;
-
-    if (tag == SCTAG_INDEX) {
-        *idp = INT_TO_JSID(int32_t(data));
-        return true;
-    }
-    if (tag == SCTAG_STRING) {
-        JSString *str = readString(data);
-        if (!str)
-            return false;
-        JSAtom *atom = AtomizeString(context(), str);
-        if (!atom)
-            return false;
-        *idp = NON_INTEGER_ATOM_TO_JSID(atom);
-        return true;
-    }
-    if (tag == SCTAG_NULL) {
-        *idp = JSID_VOID;
-        return true;
-    }
-    JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
-                         JSMSG_SC_BAD_SERIALIZED_DATA, "id");
-    return false;
-}
-
-bool
 JSStructuredCloneReader::readTransferMap()
 {
     JSContext *cx = context();
     uint64_t *headerPos = in.tell();
 
     uint32_t tag, data;
     if (!in.getPair(&tag, &data))
         return in.reportTruncated();
@@ -1585,17 +1647,17 @@ JSStructuredCloneReader::readTransferMap
                 ReportErrorTransferable(cx, callbacks);
                 return false;
             }
             if (!callbacks->readTransfer(cx, this, tag, content, extraData, closure, &obj))
                 return false;
             MOZ_ASSERT(obj);
             MOZ_ASSERT(!cx->isExceptionPending());
         }
-        
+
         // On failure, the buffer will still own the data (since its ownership will not get set to SCTAG_TMO_UNOWNED),
         // so the data will be freed by ClearStructuredClone
         if (!obj)
             return false;
 
         // Mark the SCTAG_TRANSFER_MAP_* entry as no longer owned by the input
         // buffer.
         *pos = PairToUInt64(tag, JS::SCTAG_TMO_UNOWNED);
@@ -1624,27 +1686,58 @@ JSStructuredCloneReader::read(Value *vp)
         return false;
 
     if (!startRead(vp))
         return false;
 
     while (objs.length() != 0) {
         RootedObject obj(context(), &objs.back().toObject());
 
-        RootedId id(context());
-        if (!readId(id.address()))
+        uint32_t tag, data;
+        if (!in.getPair(&tag, &data))
+            return false;
+
+        if (tag == SCTAG_END_OF_KEYS) {
+            MOZ_ALWAYS_TRUE(in.readPair(&tag, &data));
+            objs.popBack();
+            continue;
+        }
+
+        RootedValue key(context());
+        if (!startRead(key.address()))
             return false;
 
-        if (JSID_IS_VOID(id)) {
+        if (key.isNull() && !(obj->is<MapObject>() || obj->is<SetObject>())) {
+            // Backwards compatibility: Null used to indicate
+            // the end of object properties.
             objs.popBack();
+            continue;
+        }
+
+        if (obj->is<SetObject>()) {
+            if (!SetObject::add(context(), obj, key))
+                return false;
+            continue;
+        }
+
+        RootedValue val(context());
+        if (!startRead(val.address()))
+            return false;
+
+        if (obj->is<MapObject>()) {
+            if (!MapObject::set(context(), obj, key, val))
+                return false;
         } else {
-            RootedValue v(context());
-            if (!startRead(v.address()) || !JSObject::defineGeneric(context(), obj, id, v))
+            RootedId id(context());
+            if (!ValueToId<CanGC>(context(), key, &id))
                 return false;
-        }
+
+            if (!JSObject::defineGeneric(context(), obj, id, val))
+                return false;
+         }
     }
 
     allObjs.clear();
 
     return true;
 }
 
 using namespace js;