Bug 964293 - Implement Cu.cloneInto() method. r=bholley, a=1.3+
☠☠ backed out by 1a658a2342be ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Thu, 30 Jan 2014 04:45:48 -0800
changeset 175164 72dc0d6848d692f41d729298e6ec6a8879afaf95
parent 175163 9737a63a47cf0774e1744cf88f45f01c165054d6
child 175165 0980f4b18b9867dd860ae36580e8698258259430
push id3224
push userlsblakk@mozilla.com
push dateTue, 04 Feb 2014 01:06:49 +0000
treeherdermozilla-beta@60c04d0987f1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbholley, 1
bugs964293
milestone28.0a2
Bug 964293 - Implement Cu.cloneInto() method. r=bholley, a=1.3+
dom/base/StructuredCloneTags.h
dom/datastore/DataStore.jsm
dom/datastore/DataStoreCursor.jsm
js/src/jsfriendapi.cpp
js/xpconnect/idl/xpccomponents.idl
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/XPCComponents.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/tests/chrome/chrome.ini
js/xpconnect/tests/chrome/test_cloneInto.xul
--- a/dom/base/StructuredCloneTags.h
+++ b/dom/base/StructuredCloneTags.h
@@ -25,15 +25,17 @@ enum StructuredCloneTags {
   SCTAG_DOM_FILELIST,
   SCTAG_DOM_FILEHANDLE,
   SCTAG_DOM_FILE,
 
   // These tags are used for both main thread and workers.
   SCTAG_DOM_IMAGEDATA,
   SCTAG_DOM_MESSAGEPORT,
 
+  SCTAG_DOM_FUNCTION,
+
   SCTAG_DOM_MAX
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // StructuredCloneTags_h__
--- a/dom/datastore/DataStore.jsm
+++ b/dom/datastore/DataStore.jsm
@@ -20,17 +20,16 @@ const REVISION_REMOVED = "removed";
 const REVISION_VOID = "void";
 
 // This value has to be tuned a bit. Currently it's just a guess
 // and yet we don't know if it's too low or too high.
 const MAX_REQUESTS = 25;
 
 Cu.import("resource://gre/modules/DataStoreCursor.jsm");
 Cu.import("resource://gre/modules/DataStoreDB.jsm");
-Cu.import("resource://gre/modules/ObjectWrapper.jsm");
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.importGlobalProperties(["indexedDB"]);
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
@@ -137,17 +136,17 @@ this.DataStore.prototype = {
     // We're going to create this amount of requests.
     let pendingIds = aIds.length;
     let indexPos = 0;
 
     let self = this;
 
     function getInternalSuccess(aEvent, aPos) {
       debug("GetInternal success. Record: " + aEvent.target.result);
-      results[aPos] = ObjectWrapper.wrap(aEvent.target.result, self._window);
+      results[aPos] = Cu.cloneInto(aEvent.target.result, self._window);
       if (!--pendingIds) {
         aCallback(results);
         return;
       }
 
       if (indexPos < aIds.length) {
         // Just MAX_REQUESTS requests at the same time.
         let count = 0;
--- a/dom/datastore/DataStoreCursor.jsm
+++ b/dom/datastore/DataStoreCursor.jsm
@@ -22,17 +22,16 @@ const STATE_REVISION_SEND = 4;
 const STATE_DONE = 5;
 
 const REVISION_ADDED = 'added';
 const REVISION_UPDATED = 'updated';
 const REVISION_REMOVED = 'removed';
 const REVISION_VOID = 'void';
 const REVISION_SKIP = 'skip'
 
-Cu.import('resource://gre/modules/ObjectWrapper.jsm');
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 
 /**
  * legend:
  * - RID = revision ID
  * - R = revision object (with the internalRevisionId that is a number)
  * - X = current object ID.
  * - L = the list of revisions that we have to send
@@ -146,17 +145,17 @@ this.DataStoreCursor.prototype = {
     }
 
     let self = this;
     let request = aRevisionStore.openCursor(null, 'prev');
     request.onsuccess = function(aEvent) {
       self._revision = aEvent.target.result.value;
       self._objectId = 0;
       self._state = STATE_SEND_ALL;
-      aResolve(ObjectWrapper.wrap({ operation: 'clear' }, self._window));
+      aResolve(Cu.cloneInto({ operation: 'clear' }, self._window));
     }
   },
 
   stateMachineRevisionInit: function(aStore, aRevisionStore, aResolve, aReject) {
     debug('StateMachineRevisionInit');
 
     let self = this;
     let request = this._dataStore._db.getInternalRevisionId(
@@ -286,32 +285,32 @@ this.DataStoreCursor.prototype = {
     debug('StateMachineSendAll');
 
     let self = this;
     let request = aRevisionStore.openCursor(null, 'prev');
     request.onsuccess = function(aEvent) {
       if (self._revision.revisionId != aEvent.target.result.value.revisionId) {
         self._revision = aEvent.target.result.value;
         self._objectId = 0;
-        aResolve(ObjectWrapper.wrap({ operation: 'clear' }, self._window));
+        aResolve(Cu.cloneInto({ operation: 'clear' }, self._window));
         return;
       }
 
       let request = aStore.openCursor(self._window.IDBKeyRange.lowerBound(self._objectId, true));
       request.onsuccess = function(aEvent) {
         let cursor = aEvent.target.result;
         if (!cursor) {
           self._state = STATE_REVISION_CHECK;
           self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
           return;
         }
 
         self._objectId = cursor.key;
-        aResolve(ObjectWrapper.wrap({ operation: 'add', id: self._objectId,
-                                      data: cursor.value }, self._window));
+        aResolve(Cu.cloneInto({ operation: 'add', id: self._objectId,
+                                data: cursor.value }, self._window));
       };
     };
   },
 
   stateMachineRevisionSend: function(aStore, aRevisionStore, aResolve, aReject) {
     debug('StateMachineRevisionSend');
 
     if (!this._revisionsList.length) {
@@ -319,31 +318,31 @@ this.DataStoreCursor.prototype = {
       this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
       return;
     }
 
     this._revision = this._revisionsList.shift();
 
     switch (this._revision.operation) {
       case REVISION_REMOVED:
-        aResolve(ObjectWrapper.wrap({ operation: 'remove', id: this._revision.objectId },
-                                    this._window));
+        aResolve(Cu.cloneInto({ operation: 'remove', id: this._revision.objectId },
+                              this._window));
         break;
 
       case REVISION_ADDED: {
         let request = aStore.get(this._revision.objectId);
         let self = this;
         request.onsuccess = function(aEvent) {
           if (aEvent.target.result == undefined) {
             self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
             return;
           }
 
-          aResolve(ObjectWrapper.wrap({ operation: 'add', id: self._revision.objectId,
-                                        data: aEvent.target.result }, self._window));
+          aResolve(Cu.cloneInto({ operation: 'add', id: self._revision.objectId,
+                                  data: aEvent.target.result }, self._window));
         }
         break;
       }
 
       case REVISION_UPDATED: {
         let request = aStore.get(this._revision.objectId);
         let self = this;
         request.onsuccess = function(aEvent) {
@@ -352,18 +351,18 @@ this.DataStoreCursor.prototype = {
             return;
           }
 
           if (aEvent.target.result.revisionId >  self._revision.internalRevisionId) {
             self.stateMachine(aStore, aRevisionStore, aResolve, aReject);
             return;
           }
 
-          aResolve(ObjectWrapper.wrap({ operation: 'update', id: self._revision.objectId,
-                                        data: aEvent.target.result }, self._window));
+          aResolve(Cu.cloneInto({ operation: 'update', id: self._revision.objectId,
+                                  data: aEvent.target.result }, self._window));
         }
         break;
       }
 
       case REVISION_VOID:
         // Internal error!
         dump('Internal error: Revision "' + REVISION_VOID + '" should not be found!!!\n');
         break;
@@ -372,18 +371,18 @@ this.DataStoreCursor.prototype = {
         // This revision contains data that has already been sent by another one.
         this.stateMachine(aStore, aRevisionStore, aResolve, aReject);
         break;
     }
   },
 
   stateMachineDone: function(aStore, aRevisionStore, aResolve, aReject) {
     this.close();
-    aResolve(ObjectWrapper.wrap({ revisionId: this._revision.revisionId,
-                                  operation: 'done' }, this._window));
+    aResolve(Cu.cloneInto({ revisionId: this._revision.revisionId,
+                            operation: 'done' }, this._window));
   },
 
   // public interface
 
   get store() {
     return this._dataStore.exposedObject;
   },
 
--- a/js/src/jsfriendapi.cpp
+++ b/js/src/jsfriendapi.cpp
@@ -542,16 +542,17 @@ js::GetFunctionNativeReserved(JSObject *
     JS_ASSERT(fun->as<JSFunction>().isNative());
     return fun->as<JSFunction>().getExtendedSlot(which);
 }
 
 JS_FRIEND_API(void)
 js::SetFunctionNativeReserved(JSObject *fun, size_t which, const Value &val)
 {
     JS_ASSERT(fun->as<JSFunction>().isNative());
+    MOZ_ASSERT_IF(val.isObject(), val.toObject().compartment() == fun->compartment());
     fun->as<JSFunction>().setExtendedSlot(which, val);
 }
 
 JS_FRIEND_API(bool)
 js::GetObjectProto(JSContext *cx, JS::Handle<JSObject*> obj, JS::MutableHandle<JSObject*> proto)
 {
     if (IsProxy(obj))
         return JS_GetPrototype(cx, obj, proto);
--- a/js/xpconnect/idl/xpccomponents.idl
+++ b/js/xpconnect/idl/xpccomponents.idl
@@ -115,17 +115,17 @@ interface nsIXPCComponents_utils_Sandbox
 interface ScheduledGCCallback : nsISupports
 {
     void callback();
 };
 
 /**
 * interface of Components.utils
 */
-[scriptable, uuid(06530fdd-1c47-46b0-b438-ec9d696e18ce)]
+[scriptable, uuid(6a334332-a1b5-42e4-a601-863d5dfdc0d3)]
 interface nsIXPCComponents_Utils : nsISupports
 {
 
     /* reportError is designed to be called from JavaScript only.
      *
      * It will report a JS Error object to the JS console, and return. It
      * is meant for use in exception handler blocks which want to "eat"
      * an exception, but still want to report it to the console.
@@ -524,16 +524,26 @@ interface nsIXPCComponents_Utils : nsISu
       *
       * Valid categories:
       *   "RuntimeStateChange"      - Runtime switching between active and inactive states
       *   "WatchdogWakeup"          - Watchdog waking up from sleeping
       *   "WatchdogHibernateStart"  - Watchdog begins hibernating
       *   "WatchdogHibernateStop"   - Watchdog stops hibernating
       */
     PRTime getWatchdogTimestamp(in AString aCategory);
+
+    /*
+     * Clone an object into a scope.
+     * The 3rd argument is an optional options object:
+     * - cloneFunction: boolean. If true, any function in the value is are
+     *   wrapped in a function forwarder that appears to be a native function in
+     *   the content scope.
+     */
+    [implicit_jscontext]
+    jsval cloneInto(in jsval value, in jsval scope, [optional] in jsval options);
 };
 
 /**
 * interface of JavaScript's 'Components' object
 */
 [scriptable, uuid(4229848f-f49e-40e5-90cc-66968aaf4e1b)]
 interface nsIXPCComponents : nsISupports
 {
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -1784,16 +1784,27 @@ xpc::NewFunctionForwarder(JSContext *cx,
         return false;
 
     JSObject *funobj = JS_GetFunctionObject(fun);
     js::SetFunctionNativeReserved(funobj, 0, ObjectValue(*callable));
     vp.setObject(*funobj);
     return true;
 }
 
+bool
+xpc::NewFunctionForwarder(JSContext *cx, HandleObject callable, bool doclone,
+                          MutableHandleValue vp)
+{
+    RootedId emptyId(cx);
+    if (!JS_ValueToId(cx, JS_GetEmptyStringValue(cx), &emptyId))
+        return false;
+
+    return NewFunctionForwarder(cx, emptyId, callable, doclone, vp);
+}
+
 
 nsresult
 xpc::GetSandboxMetadata(JSContext *cx, HandleObject sandbox, MutableHandleValue rval)
 {
     MOZ_ASSERT(NS_IsMainThread());
     MOZ_ASSERT(IsSandbox(sandbox));
 
     RootedValue metadata(cx);
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -10,23 +10,27 @@
 #include "xpcprivate.h"
 #include "xpcIJSModuleLoader.h"
 #include "XPCJSWeakReference.h"
 #include "WrapperFactory.h"
 #include "nsJSUtils.h"
 #include "mozJSComponentLoader.h"
 #include "nsContentUtils.h"
 #include "jsfriendapi.h"
+#include "js/StructuredClone.h"
 #include "mozilla/Attributes.h"
 #include "nsJSEnvironment.h"
 #include "mozilla/XPTInterfaceInfoManager.h"
 #include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/DOMExceptionBinding.h"
 #include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/StructuredCloneTags.h"
 #include "nsZipArchive.h"
+#include "nsIDOMFile.h"
+#include "nsIDOMFileList.h"
 
 using namespace mozilla;
 using namespace JS;
 using namespace js;
 using namespace xpc;
 using mozilla::dom::Exception;
 
 /***************************************************************************/
@@ -3540,16 +3544,171 @@ nsXPCComponents_Utils::GetWatchdogTimest
     else if (aCategory.EqualsLiteral("WatchdogHibernateStop"))
         category = TimestampWatchdogHibernateStop;
     else
         return NS_ERROR_INVALID_ARG;
     *aOut = XPCJSRuntime::Get()->GetWatchdogTimestamp(category);
     return NS_OK;
 }
 
+class MOZ_STACK_CLASS CloneIntoOptions : public OptionsBase
+{
+public:
+    CloneIntoOptions(JSContext *cx = xpc_GetSafeJSContext(),
+                     JSObject *options = nullptr)
+        : OptionsBase(cx, options)
+        , cloneFunctions(false)
+    {}
+
+    virtual bool Parse()
+    {
+        return ParseBoolean("cloneFunctions", &cloneFunctions);
+    }
+
+    bool cloneFunctions;
+};
+
+class MOZ_STACK_CLASS CloneIntoCallbacksData
+{
+public:
+    CloneIntoCallbacksData(JSContext *aCx, CloneIntoOptions *aOptions)
+        : mOptions(aOptions)
+        , mFunctions(aCx)
+    {}
+
+    CloneIntoOptions *mOptions;
+    AutoObjectVector mFunctions;
+};
+
+static JSObject*
+CloneIntoReadStructuredClone(JSContext *cx,
+                             JSStructuredCloneReader *reader,
+                             uint32_t tag,
+                             uint32_t value,
+                             void* closure)
+{
+    CloneIntoCallbacksData* data = static_cast<CloneIntoCallbacksData*>(closure);
+    MOZ_ASSERT(data);
+
+    if (tag == mozilla::dom::SCTAG_DOM_BLOB || tag == mozilla::dom::SCTAG_DOM_FILELIST) {
+        MOZ_ASSERT(!value, "Data should be empty");
+
+        nsISupports *supports;
+        if (JS_ReadBytes(reader, &supports, sizeof(supports))) {
+            RootedObject global(cx, CurrentGlobalOrNull(cx));
+            if (global) {
+                RootedValue val(cx);
+                if (NS_SUCCEEDED(nsContentUtils::WrapNative(cx, global, supports, &val)))
+                    return val.toObjectOrNull();
+            }
+        }
+    }
+
+    if (tag == mozilla::dom::SCTAG_DOM_FUNCTION) {
+      MOZ_ASSERT(value < data->mFunctions.length());
+
+      RootedValue functionValue(cx);
+      RootedObject obj(cx, data->mFunctions[value]);
+
+      if (!JS_WrapObject(cx, &obj))
+          return nullptr;
+
+      if (!xpc::NewFunctionForwarder(cx, obj, false, &functionValue))
+          return nullptr;
+
+      return &functionValue.toObject();
+    }
+
+    return nullptr;
+}
+
+static bool
+CloneIntoWriteStructuredClone(JSContext *cx,
+                              JSStructuredCloneWriter *writer,
+                              HandleObject obj,
+                              void *closure)
+{
+    CloneIntoCallbacksData* data = static_cast<CloneIntoCallbacksData*>(closure);
+    MOZ_ASSERT(data);
+
+    nsCOMPtr<nsIXPConnectWrappedNative> wrappedNative;
+    nsContentUtils::XPConnect()->GetWrappedNativeOfJSObject(cx, obj, getter_AddRefs(wrappedNative));
+    if (wrappedNative) {
+        uint32_t scTag = 0;
+        nsISupports *supports = wrappedNative->Native();
+
+        nsCOMPtr<nsIDOMBlob> blob = do_QueryInterface(supports);
+        if (blob)
+            scTag = mozilla::dom::SCTAG_DOM_BLOB;
+        else {
+            nsCOMPtr<nsIDOMFileList> list = do_QueryInterface(supports);
+            if (list)
+                scTag = mozilla::dom::SCTAG_DOM_FILELIST;
+        }
+
+        if (scTag) {
+            return JS_WriteUint32Pair(writer, scTag, 0) &&
+                   JS_WriteBytes(writer, &supports, sizeof(supports));
+        }
+    }
+
+    if (data->mOptions->cloneFunctions && JS_ObjectIsCallable(cx, obj)) {
+        data->mFunctions.append(obj);
+        return JS_WriteUint32Pair(writer, mozilla::dom::SCTAG_DOM_FUNCTION,
+                                  data->mFunctions.length() - 1);
+    }
+
+    return false;
+}
+
+// These functions serialize raw XPCOM pointers in the data stream, and thus
+// should only be used when the read and write are done together
+// synchronously.
+static JSStructuredCloneCallbacks CloneIntoCallbacks = {
+    CloneIntoReadStructuredClone,
+    CloneIntoWriteStructuredClone,
+    nullptr
+};
+
+NS_IMETHODIMP
+nsXPCComponents_Utils::CloneInto(HandleValue aValue, HandleValue aScope,
+                                 HandleValue aOptions, JSContext *aCx,
+                                 MutableHandleValue aCloned)
+{
+    if (!aScope.isObject())
+        return NS_ERROR_INVALID_ARG;
+
+    RootedObject scope(aCx, &aScope.toObject());
+    scope = js::CheckedUnwrap(scope);
+    NS_ENSURE_TRUE(scope, NS_ERROR_FAILURE);
+
+    if (!aOptions.isUndefined() && !aOptions.isObject()) {
+        JS_ReportError(aCx, "Invalid argument");
+        return NS_ERROR_FAILURE;
+    }
+
+    RootedObject optionsObject(aCx, aOptions.isObject() ? &aOptions.toObject()
+                                                        : nullptr);
+    CloneIntoOptions options(aCx, optionsObject);
+    if (aOptions.isObject() && !options.Parse())
+        return NS_ERROR_FAILURE;
+
+    {
+        CloneIntoCallbacksData data(aCx, &options);
+        JSAutoCompartment ac(aCx, scope);
+        if (!JS_StructuredClone(aCx, aValue, aCloned, &CloneIntoCallbacks, &data))
+            return NS_ERROR_FAILURE;
+    }
+
+    if (!JS_WrapValue(aCx, aCloned))
+        return NS_ERROR_FAILURE;
+
+    return NS_OK;
+}
+
 /***************************************************************************/
 /***************************************************************************/
 /***************************************************************************/
 
 // XXXjband We ought to cache the wrapper in the object's slots rather than
 // re-wrapping on demand
 
 NS_INTERFACE_MAP_BEGIN(nsXPCComponents)
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3375,16 +3375,20 @@ Btoa(JSContext *cx, unsigned argc, jsval
 
 // Helper function that creates a JSFunction that wraps a native function that
 // forwards the call to the original 'callable'. If the 'doclone' argument is
 // set, it also structure clones non-native arguments for extra security.
 bool
 NewFunctionForwarder(JSContext *cx, JS::HandleId id, JS::HandleObject callable,
                      bool doclone, JS::MutableHandleValue vp);
 
+bool
+NewFunctionForwarder(JSContext *cx, JS::HandleObject callable,
+                     bool doclone, JS::MutableHandleValue vp);
+
 // Old fashioned xpc error reporter. Try to use JS_ReportError instead.
 nsresult
 ThrowAndFail(nsresult errNum, JSContext *cx, bool *retval);
 
 struct GlobalProperties {
     GlobalProperties() { mozilla::PodZero(this); }
     bool Parse(JSContext *cx, JS::HandleObject obj);
     bool Define(JSContext *cx, JS::HandleObject obj);
--- a/js/xpconnect/tests/chrome/chrome.ini
+++ b/js/xpconnect/tests/chrome/chrome.ini
@@ -48,16 +48,17 @@ support-files =
 [test_bug853571.xul]
 [test_bug858101.xul]
 [test_bug860494.xul]
 [test_bug866823.xul]
 [test_bug895340.xul]
 [test_bug932906.xul]
 [test_xrayToJS.xul]
 [test_chrometoSource.xul]
+[test_cloneInto.xul]
 [test_cows.xul]
 [test_documentdomain.xul]
 [test_doublewrappedcompartments.xul]
 [test_evalInSandbox.xul]
 [test_evalInWindow.xul]
 [test_exnstack.xul]
 [test_expandosharing.xul]
 [test_exposeInDerived.xul]
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/chrome/test_cloneInto.xul
@@ -0,0 +1,140 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<window title="Mozilla Bug 503926"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=964293"
+     target="_blank">Cu.cloneInto()</a>
+  </body>
+
+  <!-- test code goes here -->
+  <script type="application/javascript">
+  <![CDATA[
+
+  const Cu = Components.utils;
+  const Ci = Components.interfaces;
+
+  const TypedArrayThings = [
+    'Int8Array',
+    'Uint8Array',
+    'Uint8ClampedArray',
+    'Int16Array',
+    'Uint16Array',
+    'Int32Array',
+    'Uint32Array',
+    'Float32Array',
+    'Float64Array',
+  ];
+
+  function getType(a) {
+    if (a === null || a === undefined)
+      return 'null';
+
+    if (Array.isArray(a))
+      return 'array';
+
+    if (a instanceof Ci.nsIDOMFile)
+      return 'file';
+
+    if (a instanceof Ci.nsIDOMBlob)
+      return 'blob';
+
+    if (TypedArrayThings.indexOf(a.constructor.name) !== -1)
+      return a.constructor.name;
+
+    if (typeof a == 'object')
+      return 'object';
+
+    if (typeof a == 'function')
+      return 'function';
+
+    return 'primitive';
+  }
+
+  function compare(a, b) {
+    is (getType(a), getType(b), 'Type matches');
+
+    var type = getType(a);
+    if (type == 'array') {
+      is (a.length, b.length, 'Array.length matches');
+      for (var i = 0; i < a.length; ++i) {
+        compare(a[i], b[i]);
+      }
+
+      return;
+    }
+
+    if (type == 'file' || type == 'blob') {
+      ok ( a === b, 'They should match');
+      return;
+    }
+
+    if (type == 'object') {
+      ok ( a !== b, 'They should not match');
+
+      var aProps = [];
+      for (var p in a) aProps.push(p);
+
+      var bProps = [];
+      for (var p in b) bProps.push(p);
+
+      is (aProps.length, bProps.length, 'Props match');
+      is (aProps.sort().toSource(), bProps.sort().toSource(), 'Props match - using toSource()');
+
+      for (var p in a) {
+        compare(a[p], b[p]);
+      }
+
+      return;
+    }
+
+    if (type == 'function') {
+      ok ( a !== b, 'They should not match');
+      return;
+    }
+
+    if (type != 'null') {
+      is (a.toSource(), b.toSource(), 'Matching using toSource()');
+    }
+  }
+
+  var sandbox = new Cu.Sandbox(window, { sandboxPrototype: window, wantXrays: true } );
+
+  var tests = [
+    1,
+    null,
+    true,
+    'hello world',
+    [1, 2, 3],
+    { a: 1, b: 2 },
+    { blob: new Blob([]), file: new File(new Blob([])) },
+    new Date(),
+    { a: 1, b: {}, c: [1, 2, 3, { d: new Blob([]) } ], e: 'hello world' },
+  ];
+
+  for (var i = 0; i < tests.length; ++i) {
+    var test = tests[i];
+
+    var output = Cu.cloneInto(test, sandbox);
+    compare(output, test);
+  }
+
+  try {
+    var output = Cu.cloneInto({ a: function() {} }, sandbox);
+    ok(false, 'Function should not be cloned by default');
+  } catch(e) {
+    ok(true, 'Function should not be cloned by default');
+  }
+
+  var test = { a: function() { return 42; } }
+  var output = Cu.cloneInto(test, sandbox, { cloneFunctions: true });
+  compare(test, output);
+
+  ]]>
+  </script>
+</window>