https://github.com/mozilla/gecko-dev
Tip revision: 37fd0b11fb1e59ae2d1c0bc5b4e7bf033ff7d11c authored by ffxbld on 05 June 2015, 16:40:03 UTC
Added FIREFOX_38_0_6_RELEASE FIREFOX_38_0_6_BUILD1 tag(s) for changeset 76f8f33b6577. DONTBUILD CLOSED TREE a=release
Added FIREFOX_38_0_6_RELEASE FIREFOX_38_0_6_BUILD1 tag(s) for changeset 76f8f33b6577. DONTBUILD CLOSED TREE a=release
Tip revision: 37fd0b1
StructuredClone.cpp
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* vim: set ts=8 sts=4 et sw=4 tw=99:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* This file implements the structured clone algorithm of
* http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#safe-passing-of-structured-data
*
* The implementation differs slightly in that it uses an explicit stack, and
* the "memory" maps source objects to sequential integer indexes rather than
* directly pointing to destination objects. As a result, the order in which
* things are added to the memory must exactly match the order in which they
* are placed into 'allObjs', an analogous array of back-referenceable
* destination objects constructed while reading.
*
* For the most part, this is easy: simply add objects to the memory when first
* encountering them. But reading in a typed array requires an ArrayBuffer for
* construction, so objects cannot just be added to 'allObjs' in the order they
* are created. If they were, ArrayBuffers would come before typed arrays when
* in fact the typed array was added to 'memory' first.
*
* So during writing, we add objects to the memory when first encountering
* them. When reading a typed array, a placeholder is pushed onto allObjs until
* the ArrayBuffer has been read, then it is updated with the actual typed
* array object.
*/
#include "js/StructuredClone.h"
#include "mozilla/Endian.h"
#include "mozilla/FloatingPoint.h"
#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;
using mozilla::BitwiseCast;
using mozilla::IsNaN;
using mozilla::LittleEndian;
using mozilla::NativeEndian;
using JS::CanonicalizeNaN;
// When you make updates here, make sure you consider whether you need to bump the
// value of JS_STRUCTURED_CLONE_VERSION in js/public/StructuredClone.h. You will
// likely need to increment the version if anything at all changes in the serialization
// format.
//
// Note that SCTAG_END_OF_KEYS is written into the serialized form and should have
// a stable ID, it need not be at the end of the list and should not be used for
// sizing data structures.
enum StructuredDataType : uint32_t {
/* Structured data types provided by the engine */
SCTAG_FLOAT_MAX = 0xFFF00000,
SCTAG_NULL = 0xFFFF0000,
SCTAG_UNDEFINED,
SCTAG_BOOLEAN,
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_SHARED_TYPED_ARRAY_OBJECT,
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,
SCTAG_TYPED_ARRAY_V1_FLOAT64 = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Float64,
SCTAG_TYPED_ARRAY_V1_UINT8_CLAMPED = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::Uint8Clamped,
SCTAG_TYPED_ARRAY_V1_MAX = SCTAG_TYPED_ARRAY_V1_MIN + Scalar::MaxTypedArrayViewType - 1,
/*
* Define a separate range of numbers for Transferable-only tags, since
* they are not used for persistent clone buffers and therefore do not
* require bumping JS_STRUCTURED_CLONE_VERSION.
*/
SCTAG_TRANSFER_MAP_HEADER = 0xFFFF0200,
SCTAG_TRANSFER_MAP_PENDING_ENTRY,
SCTAG_TRANSFER_MAP_ARRAY_BUFFER,
SCTAG_TRANSFER_MAP_SHARED_BUFFER,
SCTAG_TRANSFER_MAP_END_OF_BUILTIN_TYPES,
SCTAG_END_OF_BUILTIN_TYPES
};
/*
* Format of transfer map:
* <SCTAG_TRANSFER_MAP_HEADER, TransferableMapHeader(UNREAD|TRANSFERRED)>
* numTransferables (64 bits)
* array of:
* <SCTAG_TRANSFER_MAP_*, TransferableOwnership>
* pointer (64 bits)
* extraData (64 bits), eg byte length for ArrayBuffers
*/
// Data associated with an SCTAG_TRANSFER_MAP_HEADER that tells whether the
// contents have been read out yet or not.
enum TransferableMapHeader {
SCTAG_TM_UNREAD = 0,
SCTAG_TM_TRANSFERRED
};
static inline uint64_t
PairToUInt64(uint32_t tag, uint32_t data)
{
return uint64_t(data) | (uint64_t(tag) << 32);
}
namespace js {
struct SCOutput {
public:
explicit SCOutput(JSContext* cx);
JSContext* context() const { return cx; }
bool write(uint64_t u);
bool writePair(uint32_t tag, uint32_t data);
bool writeDouble(double d);
bool writeBytes(const void* p, size_t nbytes);
bool writeChars(const Latin1Char* p, size_t nchars);
bool writeChars(const char16_t* p, size_t nchars);
bool writePtr(const void*);
template <class T>
bool writeArray(const T* p, size_t nbytes);
bool extractBuffer(uint64_t** datap, size_t* sizep);
uint64_t count() const { return buf.length(); }
uint64_t* rawBuffer() { return buf.begin(); }
private:
JSContext* cx;
Vector<uint64_t> buf;
};
class SCInput {
public:
SCInput(JSContext* cx, uint64_t* data, size_t nbytes);
JSContext* context() const { return cx; }
static void getPtr(const uint64_t* buffer, void** ptr);
static void getPair(const uint64_t* buffer, uint32_t* tagp, uint32_t* datap);
bool read(uint64_t* p);
bool readNativeEndian(uint64_t* p);
bool readPair(uint32_t* tagp, uint32_t* datap);
bool readDouble(double* p);
bool readBytes(void* p, size_t nbytes);
bool readChars(Latin1Char* p, size_t nchars);
bool readChars(char16_t* p, size_t nchars);
bool readPtr(void**);
bool get(uint64_t* p);
bool getPair(uint32_t* tagp, uint32_t* datap);
uint64_t* tell() const { return point; }
uint64_t* end() const { return bufEnd; }
template <class T>
bool readArray(T* p, size_t nelems);
bool reportTruncated() {
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "truncated");
return false;
}
private:
void staticAssertions() {
JS_STATIC_ASSERT(sizeof(char16_t) == 2);
JS_STATIC_ASSERT(sizeof(uint32_t) == 4);
}
JSContext* cx;
uint64_t* point;
uint64_t* bufEnd;
};
} /* namespace js */
struct JSStructuredCloneReader {
public:
explicit JSStructuredCloneReader(SCInput& in, const JSStructuredCloneCallbacks* cb,
void* cbClosure)
: in(in), objs(in.context()), allObjs(in.context()),
callbacks(cb), closure(cbClosure) { }
SCInput& input() { return in; }
bool read(MutableHandleValue vp);
private:
JSContext* context() { return in.context(); }
bool readTransferMap();
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, MutableHandleValue vp,
bool v1Read = false);
bool readSharedTypedArray(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp);
bool readArrayBuffer(uint32_t nbytes, MutableHandleValue vp);
bool readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp);
bool startRead(MutableHandleValue vp);
SCInput& in;
// Stack of objects with properties remaining to be read.
AutoValueVector objs;
// Stack of all objects read during this deserialization
AutoValueVector allObjs;
// The user defined callbacks that will be used for cloning.
const JSStructuredCloneCallbacks* callbacks;
// Any value passed to JS_ReadStructuredClone.
void* closure;
friend bool JS_ReadTypedArray(JSStructuredCloneReader* r, MutableHandleValue vp);
};
struct JSStructuredCloneWriter {
public:
explicit JSStructuredCloneWriter(JSContext* cx,
const JSStructuredCloneCallbacks* cb,
void* cbClosure,
jsval tVal)
: out(cx), objs(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(HandleValue v);
SCOutput& output() { return out; }
bool extractBuffer(uint64_t** datap, size_t* sizep) {
return out.extractBuffer(datap, sizep);
}
private:
JSContext* context() { return out.context(); }
bool writeTransferMap();
bool writeString(uint32_t tag, JSString* str);
bool writeArrayBuffer(HandleObject obj);
bool writeTypedArray(HandleObject obj);
bool writeSharedArrayBuffer(HandleObject obj);
bool writeSharedTypedArray(HandleObject obj);
bool startObject(HandleObject obj, bool* backref);
bool startWrite(HandleValue 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 entries of objs[i] remaining to be written.
// counts.length() == objs.length() and sum(counts) == entries.length().
Vector<size_t> counts;
// 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.
const JSStructuredCloneCallbacks* callbacks;
// Any value passed to JS_WriteStructuredClone.
void* closure;
// List of transferable objects
RootedValue transferable;
AutoObjectVector transferableObjects;
friend bool JS_WriteString(JSStructuredCloneWriter* w, HandleString str);
friend bool JS_WriteTypedArray(JSStructuredCloneWriter* w, HandleValue v);
};
JS_FRIEND_API(uint64_t)
js_GetSCOffset(JSStructuredCloneWriter* writer)
{
MOZ_ASSERT(writer);
return writer->output().count() * sizeof(uint64_t);
}
JS_STATIC_ASSERT(SCTAG_END_OF_BUILTIN_TYPES <= JS_SCTAG_USER_MIN);
JS_STATIC_ASSERT(JS_SCTAG_USER_MIN <= JS_SCTAG_USER_MAX);
JS_STATIC_ASSERT(Scalar::Int8 == 0);
static void
ReportErrorTransferable(JSContext* cx, const JSStructuredCloneCallbacks* callbacks)
{
if (callbacks && callbacks->reportError)
callbacks->reportError(cx, JS_SCERR_TRANSFERABLE);
else
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_SC_NOT_TRANSFERABLE);
}
bool
WriteStructuredClone(JSContext* cx, HandleValue v, uint64_t** bufp, size_t* nbytesp,
const JSStructuredCloneCallbacks* cb, void* cbClosure,
jsval transferable)
{
JSStructuredCloneWriter w(cx, cb, cbClosure, transferable);
return w.init() && w.write(v) && w.extractBuffer(bufp, nbytesp);
}
bool
ReadStructuredClone(JSContext* cx, uint64_t* data, size_t nbytes, MutableHandleValue vp,
const JSStructuredCloneCallbacks* cb, void* cbClosure)
{
SCInput in(cx, data, nbytes);
JSStructuredCloneReader r(in, cb, cbClosure);
return r.read(vp);
}
// If the given buffer contains Transferables, free them. Note that custom
// Transferables will use the JSStructuredCloneCallbacks::freeTransfer() to
// delete their transferables.
static void
Discard(uint64_t* buffer, size_t nbytes, const JSStructuredCloneCallbacks* cb, void* cbClosure)
{
MOZ_ASSERT(nbytes % sizeof(uint64_t) == 0);
if (nbytes < sizeof(uint64_t))
return; // Empty buffer
uint64_t* point = buffer;
uint32_t tag, data;
SCInput::getPair(point++, &tag, &data);
if (tag != SCTAG_TRANSFER_MAP_HEADER)
return;
if (TransferableMapHeader(data) == SCTAG_TM_TRANSFERRED)
return;
// freeTransfer should not GC
JS::AutoSuppressGCAnalysis nogc;
uint64_t numTransferables = LittleEndian::readUint64(point++);
while (numTransferables--) {
uint32_t ownership;
SCInput::getPair(point++, &tag, &ownership);
MOZ_ASSERT(tag >= SCTAG_TRANSFER_MAP_PENDING_ENTRY);
void* content;
SCInput::getPtr(point++, &content);
uint64_t extraData = LittleEndian::readUint64(point++);
if (ownership < JS::SCTAG_TMO_FIRST_OWNED)
continue;
if (ownership == JS::SCTAG_TMO_ALLOC_DATA) {
js_free(content);
} else if (ownership == JS::SCTAG_TMO_MAPPED_DATA) {
JS_ReleaseMappedArrayBufferContents(content, extraData);
} else if (ownership == JS::SCTAG_TMO_SHARED_BUFFER) {
SharedArrayRawBuffer* raw = static_cast<SharedArrayRawBuffer*>(content);
if (raw)
raw->dropReference();
} else if (cb && cb->freeTransfer) {
cb->freeTransfer(tag, JS::TransferableOwnership(ownership), content, extraData, cbClosure);
} else {
MOZ_ASSERT(false, "unknown ownership");
}
}
}
static void
ClearStructuredClone(uint64_t* data, size_t nbytes,
const JSStructuredCloneCallbacks* cb, void* cbClosure)
{
Discard(data, nbytes, cb, cbClosure);
js_free(data);
}
bool
StructuredCloneHasTransferObjects(const uint64_t* data, size_t nbytes, bool* hasTransferable)
{
*hasTransferable = false;
if (data) {
uint64_t u = LittleEndian::readUint64(data);
uint32_t tag = uint32_t(u >> 32);
if (tag == SCTAG_TRANSFER_MAP_HEADER)
*hasTransferable = true;
}
return true;
}
namespace js {
SCInput::SCInput(JSContext* cx, uint64_t* data, size_t nbytes)
: cx(cx), point(data), bufEnd(data + nbytes / 8)
{
// On 32-bit, we sometimes construct an SCInput from an SCOutput buffer,
// which is not guaranteed to be 8-byte aligned
MOZ_ASSERT((uintptr_t(data) & (sizeof(int) - 1)) == 0);
MOZ_ASSERT((nbytes & 7) == 0);
}
bool
SCInput::read(uint64_t* p)
{
if (point == bufEnd) {
*p = 0; /* initialize to shut GCC up */
return reportTruncated();
}
*p = LittleEndian::readUint64(point++);
return true;
}
bool
SCInput::readNativeEndian(uint64_t* p)
{
if (point == bufEnd) {
*p = 0; /* initialize to shut GCC up */
return reportTruncated();
}
*p = *(point++);
return true;
}
bool
SCInput::readPair(uint32_t* tagp, uint32_t* datap)
{
uint64_t u;
bool ok = read(&u);
if (ok) {
*tagp = uint32_t(u >> 32);
*datap = uint32_t(u);
}
return ok;
}
bool
SCInput::get(uint64_t* p)
{
if (point == bufEnd)
return reportTruncated();
*p = LittleEndian::readUint64(point);
return true;
}
bool
SCInput::getPair(uint32_t* tagp, uint32_t* datap)
{
uint64_t u = 0;
if (!get(&u))
return false;
*tagp = uint32_t(u >> 32);
*datap = uint32_t(u);
return true;
}
void
SCInput::getPair(const uint64_t* p, uint32_t* tagp, uint32_t* datap)
{
uint64_t u = LittleEndian::readUint64(p);
*tagp = uint32_t(u >> 32);
*datap = uint32_t(u);
}
bool
SCInput::readDouble(double* p)
{
union {
uint64_t u;
double d;
} pun;
if (!read(&pun.u))
return false;
*p = CanonicalizeNaN(pun.d);
return true;
}
template <typename T>
static void
copyAndSwapFromLittleEndian(T* dest, const void* src, size_t nelems)
{
if (nelems > 0)
NativeEndian::copyAndSwapFromLittleEndian(dest, src, nelems);
}
template <>
void
copyAndSwapFromLittleEndian(uint8_t* dest, const void* src, size_t nelems)
{
memcpy(dest, src, nelems);
}
template <class T>
bool
SCInput::readArray(T* p, size_t nelems)
{
JS_STATIC_ASSERT(sizeof(uint64_t) % sizeof(T) == 0);
/*
* Fail if nelems is so huge as to make JS_HOWMANY overflow or if nwords is
* larger than the remaining data.
*/
size_t nwords = JS_HOWMANY(nelems, sizeof(uint64_t) / sizeof(T));
if (nelems + sizeof(uint64_t) / sizeof(T) - 1 < nelems || nwords > size_t(bufEnd - point))
return reportTruncated();
copyAndSwapFromLittleEndian(p, point, nelems);
point += nwords;
return true;
}
bool
SCInput::readBytes(void* p, size_t nbytes)
{
return readArray((uint8_t*) p, nbytes);
}
bool
SCInput::readChars(Latin1Char* p, size_t nchars)
{
static_assert(sizeof(Latin1Char) == sizeof(uint8_t), "Latin1Char must fit in 1 byte");
return readBytes(p, nchars);
}
bool
SCInput::readChars(char16_t* p, size_t nchars)
{
MOZ_ASSERT(sizeof(char16_t) == sizeof(uint16_t));
return readArray((uint16_t*) p, nchars);
}
void
SCInput::getPtr(const uint64_t* p, void** ptr)
{
// No endianness conversion is used for pointers, since they are not sent
// across address spaces anyway.
*ptr = reinterpret_cast<void*>(*p);
}
bool
SCInput::readPtr(void** p)
{
uint64_t u;
if (!readNativeEndian(&u))
return false;
*p = reinterpret_cast<void*>(NativeEndian::swapFromLittleEndian(u));
return true;
}
SCOutput::SCOutput(JSContext* cx) : cx(cx), buf(cx) {}
bool
SCOutput::write(uint64_t u)
{
return buf.append(NativeEndian::swapToLittleEndian(u));
}
bool
SCOutput::writePair(uint32_t tag, uint32_t data)
{
/*
* As it happens, the tag word appears after the data word in the output.
* This is because exponents occupy the last 2 bytes of doubles on the
* little-endian platforms we care most about.
*
* For example, JSVAL_TRUE is written using writePair(SCTAG_BOOLEAN, 1).
* PairToUInt64 produces the number 0xFFFF000200000001.
* That is written out as the bytes 01 00 00 00 02 00 FF FF.
*/
return write(PairToUInt64(tag, data));
}
static inline double
ReinterpretPairAsDouble(uint32_t tag, uint32_t data)
{
return BitwiseCast<double>(PairToUInt64(tag, data));
}
bool
SCOutput::writeDouble(double d)
{
return write(BitwiseCast<uint64_t>(CanonicalizeNaN(d)));
}
template <typename T>
static void
copyAndSwapToLittleEndian(void* dest, const T* src, size_t nelems)
{
if (nelems > 0)
NativeEndian::copyAndSwapToLittleEndian(dest, src, nelems);
}
template <>
void
copyAndSwapToLittleEndian(void* dest, const uint8_t* src, size_t nelems)
{
memcpy(dest, src, nelems);
}
template <class T>
bool
SCOutput::writeArray(const T* p, size_t nelems)
{
MOZ_ASSERT(8 % sizeof(T) == 0);
MOZ_ASSERT(sizeof(uint64_t) % sizeof(T) == 0);
if (nelems == 0)
return true;
if (nelems + sizeof(uint64_t) / sizeof(T) - 1 < nelems) {
js_ReportAllocationOverflow(context());
return false;
}
size_t nwords = JS_HOWMANY(nelems, sizeof(uint64_t) / sizeof(T));
size_t start = buf.length();
if (!buf.growByUninitialized(nwords))
return false;
buf.back() = 0; /* zero-pad to an 8-byte boundary */
T* q = (T*) &buf[start];
copyAndSwapToLittleEndian(q, p, nelems);
return true;
}
bool
SCOutput::writeBytes(const void* p, size_t nbytes)
{
return writeArray((const uint8_t*) p, nbytes);
}
bool
SCOutput::writeChars(const char16_t* p, size_t nchars)
{
static_assert(sizeof(char16_t) == sizeof(uint16_t),
"required so that treating char16_t[] memory as uint16_t[] "
"memory is permissible");
return writeArray((const uint16_t*) p, nchars);
}
bool
SCOutput::writeChars(const Latin1Char* p, size_t nchars)
{
static_assert(sizeof(Latin1Char) == sizeof(uint8_t), "Latin1Char must fit in 1 byte");
return writeBytes(p, nchars);
}
bool
SCOutput::writePtr(const void* p)
{
return write(reinterpret_cast<uint64_t>(p));
}
bool
SCOutput::extractBuffer(uint64_t** datap, size_t* sizep)
{
*sizep = buf.length() * sizeof(uint64_t);
return (*datap = buf.extractRawBuffer()) != nullptr;
}
} /* namespace js */
JS_STATIC_ASSERT(JSString::MAX_LENGTH < UINT32_MAX);
JSStructuredCloneWriter::~JSStructuredCloneWriter()
{
// Free any transferable data left lying around in the buffer
uint64_t* data;
size_t size;
MOZ_ALWAYS_TRUE(extractBuffer(&data, &size));
ClearStructuredClone(data, size, callbacks, closure);
}
bool
JSStructuredCloneWriter::parseTransferable()
{
MOZ_ASSERT(transferableObjects.empty(), "parseTransferable called with stale data");
if (transferable.isNull() || transferable.isUndefined())
return true;
if (!transferable.isObject())
return reportErrorTransferable();
JSContext* cx = context();
RootedObject array(cx, &transferable.toObject());
if (!JS_IsArrayObject(cx, array))
return reportErrorTransferable();
uint32_t length;
if (!JS_GetArrayLength(cx, array, &length)) {
return false;
}
RootedValue v(context());
for (uint32_t i = 0; i < length; ++i) {
if (!JS_GetElement(cx, array, i, &v))
return false;
if (!v.isObject())
return reportErrorTransferable();
RootedObject tObj(context(), &v.toObject());
// No duplicates allowed
if (std::find(transferableObjects.begin(), transferableObjects.end(), tObj) != transferableObjects.end()) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr, JSMSG_SC_DUP_TRANSFERABLE);
return false;
}
if (!transferableObjects.append(tObj))
return false;
}
return true;
}
bool
JSStructuredCloneWriter::reportErrorTransferable()
{
ReportErrorTransferable(context(), callbacks);
return false;
}
bool
JSStructuredCloneWriter::writeString(uint32_t tag, JSString* str)
{
JSLinearString* linear = str->ensureLinear(context());
if (!linear)
return false;
static_assert(JSString::MAX_LENGTH <= INT32_MAX, "String length must fit in 31 bits");
uint32_t length = linear->length();
uint32_t lengthAndEncoding = length | (uint32_t(linear->hasLatin1Chars()) << 31);
if (!out.writePair(tag, lengthAndEncoding))
return false;
JS::AutoCheckCannotGC nogc;
return linear->hasLatin1Chars()
? out.writeChars(linear->latin1Chars(nogc), length)
: out.writeChars(linear->twoByteChars(nogc), length);
}
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);
MOZ_ASSERT(objs.length() == counts.length());
size_t total = 0;
for (size_t i = 0; i < limit; i++) {
MOZ_ASSERT(total + counts[i] >= total);
total += counts[i];
}
if (counts.length() <= MAX)
MOZ_ASSERT(total == entries.length());
else
MOZ_ASSERT(total <= entries.length());
size_t j = objs.length();
for (size_t i = 0; i < limit; i++)
MOZ_ASSERT(memory.has(&objs[--j].toObject()));
#endif
}
/*
* Write out a typed array. Note that post-v1 structured clone buffers do not
* perform endianness conversion on stored data, so multibyte typed arrays
* cannot be deserialized into a different endianness machine. Endianness
* conversion would prevent sharing ArrayBuffers: if you have Int8Array and
* Int16Array views of the same ArrayBuffer, should the data bytes be
* byte-swapped when writing or not? The Int8Array requires them to not be
* swapped; the Int16Array requires that they are.
*/
bool
JSStructuredCloneWriter::writeTypedArray(HandleObject obj)
{
Rooted<TypedArrayObject*> tarr(context(), &CheckedUnwrap(obj)->as<TypedArrayObject>());
JSAutoCompartment ac(context(), tarr);
if (!TypedArrayObject::ensureHasBuffer(context(), tarr))
return false;
if (!out.writePair(SCTAG_TYPED_ARRAY_OBJECT, tarr->length()))
return false;
uint64_t type = tarr->type();
if (!out.write(type))
return false;
// Write out the ArrayBuffer tag and contents
RootedValue val(context(), TypedArrayObject::bufferValue(tarr));
if (!startWrite(val))
return false;
return out.write(tarr->byteOffset());
}
bool
JSStructuredCloneWriter::writeArrayBuffer(HandleObject obj)
{
ArrayBufferObject& buffer = CheckedUnwrap(obj)->as<ArrayBufferObject>();
JSAutoCompartment ac(context(), &buffer);
return out.writePair(SCTAG_ARRAY_BUFFER_OBJECT, buffer.byteLength()) &&
out.writeBytes(buffer.dataPointer(), buffer.byteLength());
}
bool
JSStructuredCloneWriter::writeSharedArrayBuffer(HandleObject obj)
{
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr, JSMSG_SC_SHMEM_MUST_TRANSFER);
return false;
}
bool
JSStructuredCloneWriter::writeSharedTypedArray(HandleObject obj)
{
Rooted<SharedTypedArrayObject*> tarr(context(), &CheckedUnwrap(obj)->as<SharedTypedArrayObject>());
JSAutoCompartment ac(context(), tarr);
if (!out.writePair(SCTAG_SHARED_TYPED_ARRAY_OBJECT, tarr->length()))
return false;
uint64_t type = tarr->type();
if (!out.write(type))
return false;
// Write out the SharedArrayBuffer tag and contents.
RootedValue val(context(), SharedTypedArrayObject::bufferValue(tarr));
if (!startWrite(val))
return false;
return out.write(tarr->byteOffset());
}
bool
JSStructuredCloneWriter::startObject(HandleObject obj, bool* backref)
{
/* Handle cycles in the object graph. */
CloneMemory::AddPtr p = memory.lookupForAdd(obj);
if ((*backref = p.found()))
return out.writePair(SCTAG_BACK_REFERENCE_OBJECT, p->value());
if (!memory.add(p, obj, memory.count()))
return false;
if (memory.count() == UINT32_MAX) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_NEED_DIET, "object graph to serialize");
return false;
}
return true;
}
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.
*/
AutoIdVector properties(context());
if (!GetPropertyKeys(context(), obj, JSITER_OWNONLY, &properties))
return false;
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(properties.length()))
return false;
checkStack();
/* Write the header for obj. */
return out.writePair(ObjectClassIs(obj, ESClass_Array, context()) ? SCTAG_ARRAY_OBJECT : SCTAG_OBJECT_OBJECT, 0);
}
bool
JSStructuredCloneWriter::traverseMap(HandleObject obj)
{
AutoValueVector newEntries(context());
{
// If there is no wrapper, the compartment munging is a no-op.
RootedObject unwrapped(context(), CheckedUnwrap(obj));
MOZ_ASSERT(unwrapped);
JSAutoCompartment ac(context(), unwrapped);
if (!MapObject::getKeysAndValuesInterleaved(context(), unwrapped, &newEntries))
return false;
}
if (!context()->compartment()->wrap(context(), 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 there is no wrapper, the compartment munging is a no-op.
RootedObject unwrapped(context(), CheckedUnwrap(obj));
MOZ_ASSERT(unwrapped);
JSAutoCompartment ac(context(), unwrapped);
if (!SetObject::keys(context(), unwrapped, &keys))
return false;
}
if (!context()->compartment()->wrap(context(), 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);
}
bool
JSStructuredCloneWriter::startWrite(HandleValue v)
{
assertSameCompartment(context(), v);
if (v.isString()) {
return writeString(SCTAG_STRING, v.toString());
} 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());
bool backref;
if (!startObject(obj, &backref))
return false;
if (backref)
return true;
if (ObjectClassIs(obj, ESClass_RegExp, context())) {
RegExpGuard re(context());
if (!RegExpToShared(context(), obj, &re))
return false;
return out.writePair(SCTAG_REGEXP_OBJECT, re->getFlags()) &&
writeString(SCTAG_STRING, re->getSource());
} else if (ObjectClassIs(obj, ESClass_Date, context())) {
RootedValue unboxed(context());
if (!Unbox(context(), obj, &unboxed))
return false;
return out.writePair(SCTAG_DATE_OBJECT, 0) && out.writeDouble(unboxed.toNumber());
} else if (JS_IsTypedArrayObject(obj)) {
return writeTypedArray(obj);
} else if (JS_IsArrayBufferObject(obj) && JS_ArrayBufferHasData(obj)) {
return writeArrayBuffer(obj);
} else if (JS_IsSharedTypedArrayObject(obj)) {
return writeSharedTypedArray(obj);
} else if (JS_IsSharedArrayBufferObject(obj)) {
return writeSharedArrayBuffer(obj);
} else if (ObjectClassIs(obj, ESClass_Object, context())) {
return traverseObject(obj);
} else if (ObjectClassIs(obj, ESClass_Array, context())) {
return traverseObject(obj);
} else if (ObjectClassIs(obj, ESClass_Boolean, context())) {
RootedValue unboxed(context());
if (!Unbox(context(), obj, &unboxed))
return false;
return out.writePair(SCTAG_BOOLEAN_OBJECT, unboxed.toBoolean());
} else if (ObjectClassIs(obj, ESClass_Number, context())) {
RootedValue unboxed(context());
if (!Unbox(context(), obj, &unboxed))
return false;
return out.writePair(SCTAG_NUMBER_OBJECT, 0) && out.writeDouble(unboxed.toNumber());
} else if (ObjectClassIs(obj, ESClass_String, context())) {
RootedValue unboxed(context());
if (!Unbox(context(), obj, &unboxed))
return false;
return writeString(SCTAG_STRING_OBJECT, unboxed.toString());
} else if (ObjectClassIs(obj, ESClass_Map, context())) {
return traverseMap(obj);
} else if (ObjectClassIs(obj, ESClass_Set, context())) {
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);
return false;
}
bool
JSStructuredCloneWriter::writeTransferMap()
{
if (transferableObjects.empty())
return true;
if (!out.writePair(SCTAG_TRANSFER_MAP_HEADER, (uint32_t)SCTAG_TM_UNREAD))
return false;
if (!out.write(transferableObjects.length()))
return false;
for (JS::AutoObjectVector::Range tr = transferableObjects.all(); !tr.empty(); tr.popFront()) {
JSObject* obj = tr.front();
if (!memory.put(obj, memory.count()))
return false;
// Emit a placeholder pointer. We will steal the data and neuter the
// transferable later, in the case of ArrayBufferObject.
if (!out.writePair(SCTAG_TRANSFER_MAP_PENDING_ENTRY, JS::SCTAG_TMO_UNFILLED))
return false;
if (!out.writePtr(nullptr)) // Pointer to ArrayBuffer contents or to SharedArrayRawBuffer.
return false;
if (!out.write(0)) // extraData
return false;
}
return true;
}
bool
JSStructuredCloneWriter::transferOwnership()
{
if (transferableObjects.empty())
return true;
// Walk along the transferables and the transfer map at the same time,
// grabbing out pointers from the transferables and stuffing them into the
// transfer map.
uint64_t* point = out.rawBuffer();
MOZ_ASSERT(uint32_t(LittleEndian::readUint64(point) >> 32) == SCTAG_TRANSFER_MAP_HEADER);
point++;
MOZ_ASSERT(LittleEndian::readUint64(point) == transferableObjects.length());
point++;
for (JS::AutoObjectVector::Range tr = transferableObjects.all(); !tr.empty(); tr.popFront()) {
RootedObject obj(context(), tr.front());
uint32_t tag;
JS::TransferableOwnership ownership;
void* content;
uint64_t extraData;
#if DEBUG
SCInput::getPair(point, &tag, (uint32_t*) &ownership);
MOZ_ASSERT(tag == SCTAG_TRANSFER_MAP_PENDING_ENTRY);
MOZ_ASSERT(ownership == JS::SCTAG_TMO_UNFILLED);
#endif
if (ObjectClassIs(obj, ESClass_ArrayBuffer, context())) {
// The current setup of the array buffer inheritance hierarchy doesn't
// lend itself well to generic manipulation via proxies.
Rooted<ArrayBufferObject*> arrayBuffer(context(), &CheckedUnwrap(obj)->as<ArrayBufferObject>());
size_t nbytes = arrayBuffer->byteLength();
// Structured cloning currently only has optimizations for mapped
// and malloc'd buffers, not asm.js-ified buffers.
bool hasStealableContents = arrayBuffer->hasStealableContents() &&
(arrayBuffer->isMapped() || arrayBuffer->hasMallocedContents());
ArrayBufferObject::BufferContents bufContents =
ArrayBufferObject::stealContents(context(), arrayBuffer, hasStealableContents);
if (!bufContents)
return false; // Destructor will clean up the already-transferred data.
content = bufContents.data();
tag = SCTAG_TRANSFER_MAP_ARRAY_BUFFER;
if (bufContents.kind() == ArrayBufferObject::MAPPED)
ownership = JS::SCTAG_TMO_MAPPED_DATA;
else
ownership = JS::SCTAG_TMO_ALLOC_DATA;
extraData = nbytes;
} else if (ObjectClassIs(obj, ESClass_SharedArrayBuffer, context())) {
Rooted<SharedArrayBufferObject*> sharedArrayBuffer(context(), &CheckedUnwrap(obj)->as<SharedArrayBufferObject>());
SharedArrayRawBuffer* rawbuf = sharedArrayBuffer->rawBufferObject();
// Avoids a race condition where the parent thread frees the buffer
// before the child has accepted the transferable.
rawbuf->addReference();
tag = SCTAG_TRANSFER_MAP_SHARED_BUFFER;
ownership = JS::SCTAG_TMO_SHARED_BUFFER;
content = rawbuf;
extraData = 0;
} else {
if (!callbacks || !callbacks->writeTransfer)
return reportErrorTransferable();
if (!callbacks->writeTransfer(context(), obj, closure, &tag, &ownership, &content, &extraData))
return false;
MOZ_ASSERT(tag > SCTAG_TRANSFER_MAP_PENDING_ENTRY);
}
LittleEndian::writeUint64(point++, PairToUInt64(tag, ownership));
LittleEndian::writeUint64(point++, reinterpret_cast<uint64_t>(content));
LittleEndian::writeUint64(point++, extraData);
}
MOZ_ASSERT(point <= out.rawBuffer() + out.count());
MOZ_ASSERT_IF(point < out.rawBuffer() + out.count(),
uint32_t(LittleEndian::readUint64(point) >> 32) < SCTAG_TRANSFER_MAP_HEADER);
return true;
}
bool
JSStructuredCloneWriter::write(HandleValue v)
{
if (!startWrite(v))
return false;
while (!counts.empty()) {
RootedObject obj(context(), &objs.back().toObject());
AutoCompartment ac(context(), obj);
if (counts.back()) {
counts.back()--;
RootedValue key(context(), entries.back());
entries.popBack();
checkStack();
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 (!startWrite(key) ||
!GetProperty(context(), obj, obj, id, &val) ||
!startWrite(val))
{
return false;
}
}
}
} else {
out.writePair(SCTAG_END_OF_KEYS, 0);
objs.popBack();
counts.popBack();
}
}
memory.clear();
return transferOwnership();
}
bool
JSStructuredCloneReader::checkDouble(double d)
{
jsval_layout l;
l.asDouble = d;
if (!JSVAL_IS_DOUBLE_IMPL(l)) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "unrecognized NaN");
return false;
}
return true;
}
namespace {
template <typename CharT>
class Chars {
JSContext* cx;
CharT* p;
public:
explicit Chars(JSContext* cx) : cx(cx), p(nullptr) {}
~Chars() { js_free(p); }
bool allocate(size_t len) {
MOZ_ASSERT(!p);
// We're going to null-terminate!
p = cx->pod_malloc<CharT>(len + 1);
if (p) {
p[len] = CharT(0);
return true;
}
return false;
}
CharT* get() { return p; }
void forget() { p = nullptr; }
};
} /* anonymous namespace */
template <typename CharT>
JSString*
JSStructuredCloneReader::readStringImpl(uint32_t nchars)
{
if (nchars > JSString::MAX_LENGTH) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "string length");
return nullptr;
}
Chars<CharT> chars(context());
if (!chars.allocate(nchars) || !in.readChars(chars.get(), nchars))
return nullptr;
JSString* str = NewString<CanGC>(context(), chars.get(), nchars);
if (str)
chars.forget();
return str;
}
JSString*
JSStructuredCloneReader::readString(uint32_t data)
{
uint32_t nchars = data & JS_BITMASK(31);
bool latin1 = data & (1 << 31);
return latin1 ? readStringImpl<Latin1Char>(nchars) : readStringImpl<char16_t>(nchars);
}
static uint32_t
TagToV1ArrayType(uint32_t tag)
{
MOZ_ASSERT(tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX);
return tag - SCTAG_TYPED_ARRAY_V1_MIN;
}
bool
JSStructuredCloneReader::readTypedArray(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp,
bool v1Read)
{
if (arrayType > Scalar::Uint8Clamped) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "unhandled typed array element type");
return false;
}
// Push a placeholder onto the allObjs list to stand in for the typed array
uint32_t placeholderIndex = allObjs.length();
Value dummy = UndefinedValue();
if (!allObjs.append(dummy))
return false;
// Read the ArrayBuffer object and its contents (but no properties)
RootedValue v(context());
uint32_t byteOffset;
if (v1Read) {
if (!readV1ArrayBuffer(arrayType, nelems, &v))
return false;
byteOffset = 0;
} else {
if (!startRead(&v))
return false;
uint64_t n;
if (!in.read(&n))
return false;
byteOffset = n;
}
RootedObject buffer(context(), &v.toObject());
RootedObject obj(context(), nullptr);
switch (arrayType) {
case Scalar::Int8:
obj = JS_NewInt8ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Uint8:
obj = JS_NewUint8ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Int16:
obj = JS_NewInt16ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Uint16:
obj = JS_NewUint16ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Int32:
obj = JS_NewInt32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Uint32:
obj = JS_NewUint32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Float32:
obj = JS_NewFloat32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Float64:
obj = JS_NewFloat64ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Uint8Clamped:
obj = JS_NewUint8ClampedArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
default:
MOZ_CRASH("Can't happen: arrayType range checked above");
}
if (!obj)
return false;
vp.setObject(*obj);
allObjs[placeholderIndex].set(vp);
return true;
}
bool
JSStructuredCloneReader::readSharedTypedArray(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp)
{
if (arrayType > Scalar::Uint8Clamped) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "unhandled typed array element type");
return false;
}
// Push a placeholder onto the allObjs list to stand in for the typed array.
uint32_t placeholderIndex = allObjs.length();
Value dummy = UndefinedValue();
if (!allObjs.append(dummy))
return false;
// Read the ArrayBuffer object and its contents (but no properties).
RootedValue v(context());
uint32_t byteOffset;
if (!startRead(&v))
return false;
uint64_t n;
if (!in.read(&n))
return false;
byteOffset = n;
RootedObject buffer(context(), &v.toObject());
RootedObject obj(context());
switch (arrayType) {
case Scalar::Int8:
obj = JS_NewSharedInt8ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Uint8:
obj = JS_NewSharedUint8ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Int16:
obj = JS_NewSharedInt16ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Uint16:
obj = JS_NewSharedUint16ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Int32:
obj = JS_NewSharedInt32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Uint32:
obj = JS_NewSharedUint32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Float32:
obj = JS_NewSharedFloat32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Float64:
obj = JS_NewSharedFloat64ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case Scalar::Uint8Clamped:
obj = JS_NewSharedUint8ClampedArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
default:
MOZ_CRASH("Can't happen: arrayType range checked above");
}
if (!obj)
return false;
vp.setObject(*obj);
allObjs[placeholderIndex].set(vp);
return true;
}
bool
JSStructuredCloneReader::readArrayBuffer(uint32_t nbytes, MutableHandleValue vp)
{
JSObject* obj = ArrayBufferObject::create(context(), nbytes);
if (!obj)
return false;
vp.setObject(*obj);
ArrayBufferObject& buffer = obj->as<ArrayBufferObject>();
MOZ_ASSERT(buffer.byteLength() == nbytes);
return in.readArray(buffer.dataPointer(), nbytes);
}
/*
* Read in the data for a structured clone version 1 ArrayBuffer, performing
* endianness-conversion while reading.
*/
bool
JSStructuredCloneReader::readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems,
MutableHandleValue vp)
{
MOZ_ASSERT(arrayType <= Scalar::Uint8Clamped);
uint32_t nbytes = nelems << TypedArrayShift(static_cast<Scalar::Type>(arrayType));
JSObject* obj = ArrayBufferObject::create(context(), nbytes);
if (!obj)
return false;
vp.setObject(*obj);
ArrayBufferObject& buffer = obj->as<ArrayBufferObject>();
MOZ_ASSERT(buffer.byteLength() == nbytes);
switch (arrayType) {
case Scalar::Int8:
case Scalar::Uint8:
case Scalar::Uint8Clamped:
return in.readArray((uint8_t*) buffer.dataPointer(), nelems);
case Scalar::Int16:
case Scalar::Uint16:
return in.readArray((uint16_t*) buffer.dataPointer(), nelems);
case Scalar::Int32:
case Scalar::Uint32:
case Scalar::Float32:
return in.readArray((uint32_t*) buffer.dataPointer(), nelems);
case Scalar::Float64:
return in.readArray((uint64_t*) buffer.dataPointer(), nelems);
default:
MOZ_CRASH("Can't happen: arrayType range checked by caller");
}
}
static bool
PrimitiveToObject(JSContext* cx, MutableHandleValue vp)
{
JSObject* obj = js::PrimitiveToObject(cx, vp);
if (!obj)
return false;
vp.setObject(*obj);
return true;
}
bool
JSStructuredCloneReader::startRead(MutableHandleValue vp)
{
uint32_t tag, data;
if (!in.readPair(&tag, &data))
return false;
switch (tag) {
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:
case SCTAG_STRING_OBJECT: {
JSString* str = readString(data);
if (!str)
return false;
vp.setString(str);
if (tag == SCTAG_STRING_OBJECT && !PrimitiveToObject(context(), vp))
return false;
break;
}
case SCTAG_NUMBER_OBJECT: {
double d;
if (!in.readDouble(&d) || !checkDouble(d))
return false;
vp.setDouble(d);
if (!PrimitiveToObject(context(), vp))
return false;
break;
}
case SCTAG_DATE_OBJECT: {
double d;
if (!in.readDouble(&d) || !checkDouble(d))
return false;
if (!IsNaN(d) && d != TimeClip(d)) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "date");
return false;
}
JSObject* obj = js_NewDateObjectMsec(context(), d);
if (!obj)
return false;
vp.setObject(*obj);
break;
}
case SCTAG_REGEXP_OBJECT: {
RegExpFlag flags = RegExpFlag(data);
uint32_t tag2, stringData;
if (!in.readPair(&tag2, &stringData))
return false;
if (tag2 != SCTAG_STRING) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "regexp");
return false;
}
JSString* str = readString(stringData);
if (!str)
return false;
RootedAtom atom(context(), AtomizeString(context(), str));
if (!atom)
return false;
RegExpObject* reobj = RegExpObject::createNoStatics(context(), atom, flags, nullptr,
context()->tempLifoAlloc());
if (!reobj)
return false;
vp.setObject(*reobj);
break;
}
case SCTAG_ARRAY_OBJECT:
case SCTAG_OBJECT_OBJECT: {
JSObject* obj = (tag == SCTAG_ARRAY_OBJECT)
? (JSObject*) NewDenseEmptyArray(context())
: (JSObject*) NewBuiltinClassInstance<PlainObject>(context());
if (!obj || !objs.append(ObjectValue(*obj)))
return false;
vp.setObject(*obj);
break;
}
case SCTAG_BACK_REFERENCE_OBJECT: {
if (data >= allObjs.length()) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA,
"invalid back reference in input");
return false;
}
vp.set(allObjs[data]);
return true;
}
case SCTAG_TRANSFER_MAP_HEADER:
case SCTAG_TRANSFER_MAP_PENDING_ENTRY:
// We should be past all the transfer map tags.
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL,
JSMSG_SC_BAD_SERIALIZED_DATA,
"invalid input");
return false;
case SCTAG_ARRAY_BUFFER_OBJECT:
if (!readArrayBuffer(data, vp))
return false;
break;
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_SHARED_TYPED_ARRAY_OBJECT: {
// readSharedTypedArray adds the array to allObjs.
uint64_t arrayType;
if (!in.read(&arrayType))
return false;
return readSharedTypedArray(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;
}
if (SCTAG_TYPED_ARRAY_V1_MIN <= tag && tag <= SCTAG_TYPED_ARRAY_V1_MAX) {
// A v1-format typed array
// readTypedArray adds the array to allObjs
return readTypedArray(TagToV1ArrayType(tag), data, vp, true);
}
if (!callbacks || !callbacks->read) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "unsupported type");
return false;
}
JSObject* obj = callbacks->read(context(), this, tag, data, closure);
if (!obj)
return false;
vp.setObject(*obj);
}
}
if (vp.isObject() && !allObjs.append(vp))
return false;
return true;
}
bool
JSStructuredCloneReader::readTransferMap()
{
JSContext* cx = context();
uint64_t* headerPos = in.tell();
uint32_t tag, data;
if (!in.getPair(&tag, &data))
return in.reportTruncated();
if (tag != SCTAG_TRANSFER_MAP_HEADER || TransferableMapHeader(data) == SCTAG_TM_TRANSFERRED)
return true;
uint64_t numTransferables;
MOZ_ALWAYS_TRUE(in.readPair(&tag, &data));
if (!in.read(&numTransferables))
return false;
for (uint64_t i = 0; i < numTransferables; i++) {
uint64_t* pos = in.tell();
if (!in.readPair(&tag, &data))
return false;
MOZ_ASSERT(tag != SCTAG_TRANSFER_MAP_PENDING_ENTRY);
RootedObject obj(cx);
void* content;
if (!in.readPtr(&content))
return false;
uint64_t extraData;
if (!in.read(&extraData))
return false;
if (tag == SCTAG_TRANSFER_MAP_ARRAY_BUFFER) {
size_t nbytes = extraData;
MOZ_ASSERT(data == JS::SCTAG_TMO_ALLOC_DATA ||
data == JS::SCTAG_TMO_MAPPED_DATA);
if (data == JS::SCTAG_TMO_ALLOC_DATA)
obj = JS_NewArrayBufferWithContents(cx, nbytes, content);
else if (data == JS::SCTAG_TMO_MAPPED_DATA)
obj = JS_NewMappedArrayBufferWithContents(cx, nbytes, content);
} else if (tag == SCTAG_TRANSFER_MAP_SHARED_BUFFER) {
MOZ_ASSERT(data == JS::SCTAG_TMO_SHARED_BUFFER);
obj = SharedArrayBufferObject::New(context(), (SharedArrayRawBuffer*)content);
} else {
if (!callbacks || !callbacks->readTransfer) {
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);
MOZ_ASSERT(headerPos < pos && pos < in.end());
if (!allObjs.append(ObjectValue(*obj)))
return false;
}
// Mark the whole transfer map as consumed.
MOZ_ASSERT(headerPos <= in.tell());
#ifdef DEBUG
SCInput::getPair(headerPos, &tag, &data);
MOZ_ASSERT(tag == SCTAG_TRANSFER_MAP_HEADER);
MOZ_ASSERT(TransferableMapHeader(data) != SCTAG_TM_TRANSFERRED);
#endif
*headerPos = PairToUInt64(SCTAG_TRANSFER_MAP_HEADER, SCTAG_TM_TRANSFERRED);
return true;
}
bool
JSStructuredCloneReader::read(MutableHandleValue vp)
{
if (!readTransferMap())
return false;
if (!startRead(vp))
return false;
while (objs.length() != 0) {
RootedObject obj(context(), &objs.back().toObject());
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))
return false;
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))
return false;
if (obj->is<MapObject>()) {
if (!MapObject::set(context(), obj, key, val))
return false;
} else {
RootedId id(context());
if (!ValueToId<CanGC>(context(), key, &id))
return false;
if (!DefineProperty(context(), obj, id, val))
return false;
}
}
allObjs.clear();
return true;
}
using namespace js;
JS_PUBLIC_API(bool)
JS_ReadStructuredClone(JSContext* cx, uint64_t* buf, size_t nbytes,
uint32_t version, MutableHandleValue vp,
const JSStructuredCloneCallbacks* optionalCallbacks,
void* closure)
{
AssertHeapIsIdle(cx);
CHECK_REQUEST(cx);
if (version > JS_STRUCTURED_CLONE_VERSION) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_SC_BAD_CLONE_VERSION);
return false;
}
const JSStructuredCloneCallbacks* callbacks =
optionalCallbacks ?
optionalCallbacks :
cx->runtime()->structuredCloneCallbacks;
return ReadStructuredClone(cx, buf, nbytes, vp, callbacks, closure);
}
JS_PUBLIC_API(bool)
JS_WriteStructuredClone(JSContext* cx, HandleValue value, uint64_t** bufp, size_t* nbytesp,
const JSStructuredCloneCallbacks* optionalCallbacks,
void* closure, HandleValue transferable)
{
AssertHeapIsIdle(cx);
CHECK_REQUEST(cx);
assertSameCompartment(cx, value);
const JSStructuredCloneCallbacks* callbacks =
optionalCallbacks ?
optionalCallbacks :
cx->runtime()->structuredCloneCallbacks;
return WriteStructuredClone(cx, value, bufp, nbytesp, callbacks, closure, transferable);
}
JS_PUBLIC_API(bool)
JS_ClearStructuredClone(uint64_t* data, size_t nbytes,
const JSStructuredCloneCallbacks* optionalCallbacks,
void* closure)
{
ClearStructuredClone(data, nbytes, optionalCallbacks, closure);
return true;
}
JS_PUBLIC_API(bool)
JS_StructuredCloneHasTransferables(const uint64_t* data, size_t nbytes,
bool* hasTransferable)
{
bool transferable;
if (!StructuredCloneHasTransferObjects(data, nbytes, &transferable))
return false;
*hasTransferable = transferable;
return true;
}
JS_PUBLIC_API(bool)
JS_StructuredClone(JSContext* cx, HandleValue value, MutableHandleValue vp,
const JSStructuredCloneCallbacks* optionalCallbacks,
void* closure)
{
AssertHeapIsIdle(cx);
CHECK_REQUEST(cx);
// Strings are associated with zones, not compartments,
// so we copy the string by wrapping it.
if (value.isString()) {
RootedString strValue(cx, value.toString());
if (!cx->compartment()->wrap(cx, &strValue)) {
return false;
}
vp.setString(strValue);
return true;
}
const JSStructuredCloneCallbacks* callbacks =
optionalCallbacks ?
optionalCallbacks :
cx->runtime()->structuredCloneCallbacks;
JSAutoStructuredCloneBuffer buf;
{
// If we use Maybe<AutoCompartment> here, G++ can't tell that the
// destructor is only called when Maybe::construct was called, and
// we get warnings about using uninitialized variables.
if (value.isObject()) {
AutoCompartment ac(cx, &value.toObject());
if (!buf.write(cx, value, callbacks, closure))
return false;
} else {
if (!buf.write(cx, value, callbacks, closure))
return false;
}
}
return buf.read(cx, vp, callbacks, closure);
}
JSAutoStructuredCloneBuffer::JSAutoStructuredCloneBuffer(JSAutoStructuredCloneBuffer&& other)
{
other.steal(&data_, &nbytes_, &version_);
}
JSAutoStructuredCloneBuffer&
JSAutoStructuredCloneBuffer::operator=(JSAutoStructuredCloneBuffer&& other)
{
MOZ_ASSERT(&other != this);
clear();
other.steal(&data_, &nbytes_, &version_);
return *this;
}
void
JSAutoStructuredCloneBuffer::clear()
{
if (data_) {
ClearStructuredClone(data_, nbytes_, callbacks_, closure_);
data_ = nullptr;
nbytes_ = 0;
version_ = 0;
}
}
bool
JSAutoStructuredCloneBuffer::copy(const uint64_t* srcData, size_t nbytes, uint32_t version)
{
// transferable objects cannot be copied
bool hasTransferable;
if (!StructuredCloneHasTransferObjects(data_, nbytes_, &hasTransferable) ||
hasTransferable)
return false;
uint64_t* newData = static_cast<uint64_t*>(js_malloc(nbytes));
if (!newData)
return false;
js_memcpy(newData, srcData, nbytes);
clear();
data_ = newData;
nbytes_ = nbytes;
version_ = version;
return true;
}
void
JSAutoStructuredCloneBuffer::adopt(uint64_t* data, size_t nbytes, uint32_t version)
{
clear();
data_ = data;
nbytes_ = nbytes;
version_ = version;
}
void
JSAutoStructuredCloneBuffer::steal(uint64_t** datap, size_t* nbytesp, uint32_t* versionp)
{
*datap = data_;
*nbytesp = nbytes_;
if (versionp)
*versionp = version_;
data_ = nullptr;
nbytes_ = 0;
version_ = 0;
}
bool
JSAutoStructuredCloneBuffer::read(JSContext* cx, MutableHandleValue vp,
const JSStructuredCloneCallbacks* optionalCallbacks,
void* closure)
{
MOZ_ASSERT(cx);
MOZ_ASSERT(data_);
return !!JS_ReadStructuredClone(cx, data_, nbytes_, version_, vp,
optionalCallbacks, closure);
}
bool
JSAutoStructuredCloneBuffer::write(JSContext* cx, HandleValue value,
const JSStructuredCloneCallbacks* optionalCallbacks,
void* closure)
{
HandleValue transferable = UndefinedHandleValue;
return write(cx, value, transferable, optionalCallbacks, closure);
}
bool
JSAutoStructuredCloneBuffer::write(JSContext* cx, HandleValue value,
HandleValue transferable,
const JSStructuredCloneCallbacks* optionalCallbacks,
void* closure)
{
clear();
bool ok = !!JS_WriteStructuredClone(cx, value, &data_, &nbytes_,
optionalCallbacks, closure,
transferable);
if (!ok) {
data_ = nullptr;
nbytes_ = 0;
version_ = JS_STRUCTURED_CLONE_VERSION;
}
return ok;
}
JS_PUBLIC_API(void)
JS_SetStructuredCloneCallbacks(JSRuntime* rt, const JSStructuredCloneCallbacks* callbacks)
{
rt->structuredCloneCallbacks = callbacks;
}
JS_PUBLIC_API(bool)
JS_ReadUint32Pair(JSStructuredCloneReader* r, uint32_t* p1, uint32_t* p2)
{
return r->input().readPair((uint32_t*) p1, (uint32_t*) p2);
}
JS_PUBLIC_API(bool)
JS_ReadBytes(JSStructuredCloneReader* r, void* p, size_t len)
{
return r->input().readBytes(p, len);
}
JS_PUBLIC_API(bool)
JS_ReadTypedArray(JSStructuredCloneReader* r, MutableHandleValue vp)
{
uint32_t tag, nelems;
if (!r->input().readPair(&tag, &nelems))
return false;
if (tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX) {
return r->readTypedArray(TagToV1ArrayType(tag), nelems, vp, true);
} else if (tag == SCTAG_TYPED_ARRAY_OBJECT) {
uint64_t arrayType;
if (!r->input().read(&arrayType))
return false;
return r->readTypedArray(arrayType, nelems, vp);
} else {
JS_ReportErrorNumber(r->context(), js_GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "expected type array");
return false;
}
}
JS_PUBLIC_API(bool)
JS_WriteUint32Pair(JSStructuredCloneWriter* w, uint32_t tag, uint32_t data)
{
return w->output().writePair(tag, data);
}
JS_PUBLIC_API(bool)
JS_WriteBytes(JSStructuredCloneWriter* w, const void* p, size_t len)
{
return w->output().writeBytes(p, len);
}
JS_PUBLIC_API(bool)
JS_WriteString(JSStructuredCloneWriter* w, HandleString str)
{
return w->writeString(SCTAG_STRING, str);
}
JS_PUBLIC_API(bool)
JS_WriteTypedArray(JSStructuredCloneWriter* w, HandleValue v)
{
MOZ_ASSERT(v.isObject());
assertSameCompartment(w->context(), v);
RootedObject obj(w->context(), &v.toObject());
return w->writeTypedArray(obj);
}