blob: e1ec72b641ee414949092bc681858277b716c9b9 [file] [log] [blame]
/* -*- 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/EndianUtils.h"
#include "mozilla/FloatingPoint.h"
#include <algorithm>
#include "jsapi.h"
#include "jscntxt.h"
#include "jsdate.h"
#include "jswrapper.h"
#include "builtin/MapObject.h"
#include "js/Date.h"
#include "js/GCHashTable.h"
#include "vm/SavedFrame.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 mozilla::NumbersAreIdentical;
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_DATA_VIEW_OBJECT,
SCTAG_SAVED_FRAME_OBJECT,
SCTAG_JSPRINCIPALS,
SCTAG_NULL_JSPRINCIPALS,
SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM,
SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM,
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, 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 readDataView(uint32_t byteLength, MutableHandleValue vp);
bool readArrayBuffer(uint32_t nbytes, MutableHandleValue vp);
bool readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems, MutableHandleValue vp);
JSObject* readSavedFrame(uint32_t principalsTag);
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,
Value tVal)
: out(cx), objs(out.context()),
counts(out.context()), entries(out.context()),
memory(out.context(), CloneMemory(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:
JSStructuredCloneWriter() = delete;
JSStructuredCloneWriter(const JSStructuredCloneWriter&) = delete;
JSContext* context() { return out.context(); }
bool writeTransferMap();
bool writeString(uint32_t tag, JSString* str);
bool writeArrayBuffer(HandleObject obj);
bool writeTypedArray(HandleObject obj);
bool writeDataView(HandleObject obj);
bool writeSharedArrayBuffer(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 traverseSavedFrame(HandleObject obj);
bool parseTransferable();
bool reportErrorTransferable(uint32_t errorId);
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: Property IDs as value
// For Map: Key followed by value
// For Set: Key
// For SavedFrame: parent SavedFrame
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
using CloneMemory = GCHashMap<JSObject*, uint32_t, MovableCellHasher<JSObject*>>;
Rooted<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,
uint32_t errorId)
{
if (callbacks && callbacks->reportError)
callbacks->reportError(cx, errorId);
else if (errorId == JS_SCERR_DUP_TRANSFERABLE)
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_SC_DUP_TRANSFERABLE);
else
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_SC_NOT_TRANSFERABLE);
}
bool
WriteStructuredClone(JSContext* cx, HandleValue v, uint64_t** bufp, size_t* nbytesp,
const JSStructuredCloneCallbacks* cb, void* cbClosure,
Value 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
DiscardTransferables(uint64_t* buffer, size_t nbytes,
const JSStructuredCloneCallbacks* cb, void* cbClosure)
{
MOZ_ASSERT(nbytes % sizeof(uint64_t) == 0);
uint64_t* end = buffer + nbytes / sizeof(uint64_t);
uint64_t* point = buffer;
if (point == end)
return; // Empty 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;
if (point == end)
return;
uint64_t numTransferables = LittleEndian::readUint64(point++);
while (numTransferables--) {
if (point == end)
return;
uint32_t ownership;
SCInput::getPair(point++, &tag, &ownership);
MOZ_ASSERT(tag >= SCTAG_TRANSFER_MAP_PENDING_ENTRY);
if (point == end)
return;
void* content;
SCInput::getPtr(point++, &content);
if (point == end)
return;
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 bool
StructuredCloneHasTransferObjects(const uint64_t* data, size_t nbytes)
{
if (!data)
return false;
uint64_t u = LittleEndian::readUint64(data);
uint32_t tag = uint32_t(u >> 32);
return (tag == SCTAG_TRANSFER_MAP_HEADER);
}
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, TrueValue() 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) {
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;
{
AutoEnterOOMUnsafeRegion oomUnsafe;
if (!extractBuffer(&data, &size))
oomUnsafe.crash("Unable to extract clone buffer");
DiscardTransferables(data, size, callbacks, closure);
js_free(data);
}
}
bool
JSStructuredCloneWriter::parseTransferable()
{
MOZ_ASSERT(transferableObjects.empty(), "parseTransferable called with stale data");
if (transferable.isNull() || transferable.isUndefined())
return true;
if (!transferable.isObject())
return reportErrorTransferable(JS_SCERR_TRANSFERABLE);
JSContext* cx = context();
RootedObject array(cx, &transferable.toObject());
bool isArray;
if (!JS_IsArrayObject(cx, array, &isArray))
return false;
if (!isArray)
return reportErrorTransferable(JS_SCERR_TRANSFERABLE);
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(JS_SCERR_TRANSFERABLE);
RootedObject tObj(context(), &v.toObject());
// No duplicates allowed
if (std::find(transferableObjects.begin(), transferableObjects.end(), tObj) != transferableObjects.end()) {
return reportErrorTransferable(JS_SCERR_DUP_TRANSFERABLE);
}
if (!transferableObjects.append(tObj))
return false;
}
return true;
}
bool
JSStructuredCloneWriter::reportErrorTransferable(uint32_t errorId)
{
ReportErrorTransferable(context(), callbacks, errorId);
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::writeDataView(HandleObject obj)
{
Rooted<DataViewObject*> view(context(), &CheckedUnwrap(obj)->as<DataViewObject>());
JSAutoCompartment ac(context(), view);
if (!out.writePair(SCTAG_DATA_VIEW_OBJECT, view->byteLength()))
return false;
// Write out the ArrayBuffer tag and contents
RootedValue val(context(), DataViewObject::bufferValue(view));
if (!startWrite(val))
return false;
return out.write(view->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(), GetErrorMessage, nullptr, JSMSG_SC_SHMEM_MUST_TRANSFER);
return false;
}
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(), 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. */
ESClassValue cls;
if (!GetBuiltinClass(context(), obj, &cls))
return false;
return out.writePair(cls == ESClass_Array ? 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);
}
// Objects are written as a "preorder" traversal of the object graph: object
// "headers" (the class tag and any data needed for initial construction) are
// visited first, then the children are recursed through (where children are
// properties, Set or Map entries, etc.). So for example
//
// m = new Map();
// m.set(key1 = {}, value1 = {})
//
// would be stored as
//
// <Map tag>
// <key1 class tag>
// <value1 class tag>
// <end-of-children marker for key1>
// <end-of-children marker for value1>
// <end-of-children marker for Map>
//
// Notice how the end-of-children marker for key1 is sandwiched between the
// value1 beginning and end.
bool
JSStructuredCloneWriter::traverseSavedFrame(HandleObject obj)
{
RootedObject unwrapped(context(), js::CheckedUnwrap(obj));
MOZ_ASSERT(unwrapped && unwrapped->is<SavedFrame>());
RootedSavedFrame savedFrame(context(), &unwrapped->as<SavedFrame>());
RootedObject parent(context(), savedFrame->getParent());
if (!context()->compartment()->wrap(context(), &parent))
return false;
if (!objs.append(ObjectValue(*obj)) ||
!entries.append(parent ? ObjectValue(*parent) : NullValue()) ||
!counts.append(1))
{
return false;
}
checkStack();
// Write the SavedFrame tag and the SavedFrame's principals.
if (savedFrame->getPrincipals() == &ReconstructedSavedFramePrincipals::IsSystem) {
if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT,
SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM))
{
return false;
};
} else if (savedFrame->getPrincipals() == &ReconstructedSavedFramePrincipals::IsNotSystem) {
if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT,
SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM))
{
return false;
}
} else {
if (auto principals = savedFrame->getPrincipals()) {
if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_JSPRINCIPALS) ||
!principals->write(context(), this))
{
return false;
}
} else {
if (!out.writePair(SCTAG_SAVED_FRAME_OBJECT, SCTAG_NULL_JSPRINCIPALS))
return false;
}
}
// Write the SavedFrame's reserved slots, except for the parent, which is
// queued on objs for further traversal.
RootedValue val(context());
val = StringValue(savedFrame->getSource());
if (!startWrite(val))
return false;
val = NumberValue(savedFrame->getLine());
if (!startWrite(val))
return false;
val = NumberValue(savedFrame->getColumn());
if (!startWrite(val))
return false;
auto name = savedFrame->getFunctionDisplayName();
val = name ? StringValue(name) : NullValue();
if (!startWrite(val))
return false;
auto cause = savedFrame->getAsyncCause();
val = cause ? StringValue(cause) : NullValue();
if (!startWrite(val))
return false;
return true;
}
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;
ESClassValue cls;
if (!GetBuiltinClass(context(), obj, &cls))
return false;
if (cls == ESClass_RegExp) {
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 (cls == ESClass_Date) {
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_IsDataViewObject(obj)) {
return writeDataView(obj);
} else if (JS_IsArrayBufferObject(obj) && JS_ArrayBufferHasData(obj)) {
return writeArrayBuffer(obj);
} else if (JS_IsSharedArrayBufferObject(obj)) {
return writeSharedArrayBuffer(obj);
} else if (cls == ESClass_Object) {
return traverseObject(obj);
} else if (cls == ESClass_Array) {
return traverseObject(obj);
} else if (cls == ESClass_Boolean) {
RootedValue unboxed(context());
if (!Unbox(context(), obj, &unboxed))
return false;
return out.writePair(SCTAG_BOOLEAN_OBJECT, unboxed.toBoolean());
} else if (cls == ESClass_Number) {
RootedValue unboxed(context());
if (!Unbox(context(), obj, &unboxed))
return false;
return out.writePair(SCTAG_NUMBER_OBJECT, 0) && out.writeDouble(unboxed.toNumber());
} else if (cls == ESClass_String) {
RootedValue unboxed(context());
if (!Unbox(context(), obj, &unboxed))
return false;
return writeString(SCTAG_STRING_OBJECT, unboxed.toString());
} else if (cls == ESClass_Map) {
return traverseMap(obj);
} else if (cls == ESClass_Set) {
return traverseSet(obj);
} else if (SavedFrame::isSavedFrameOrWrapperAndNotProto(*obj)) {
return traverseSavedFrame(obj);
}
if (callbacks && callbacks->write)
return callbacks->write(context(), this, obj, closure);
/* else fall through */
}
JS_ReportErrorNumber(context(), 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
ESClassValue cls;
if (!GetBuiltinClass(context(), obj, &cls))
return false;
if (cls == ESClass_ArrayBuffer) {
// 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 (cls == ESClass_SharedArrayBuffer) {
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(JS_SCERR_TRANSFERABLE);
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();
ESClassValue cls;
if (!GetBuiltinClass(context(), obj, &cls))
return false;
if (cls == ESClass_Map) {
counts.back()--;
RootedValue val(context(), entries.back());
entries.popBack();
checkStack();
if (!startWrite(key) || !startWrite(val))
return false;
} else if (cls == ESClass_Set || SavedFrame::isSavedFrameOrWrapperAndNotProto(*obj)) {
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(), 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(), 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(), 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::readDataView(uint32_t byteLength, MutableHandleValue vp)
{
// Push a placeholder onto the allObjs list to stand in for the DataView.
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());
if (!startRead(&v))
return false;
// Read byteOffset.
uint64_t n;
if (!in.read(&n))
return false;
uint32_t byteOffset = n;
RootedObject buffer(context(), &v.toObject());
RootedObject obj(context(), JS_NewDataView(context(), buffer, byteOffset, byteLength));
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;
JS::ClippedTime t = JS::TimeClip(d);
if (!NumbersAreIdentical(d, t.toDouble())) {
JS_ReportErrorNumber(context(), GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "date");
return false;
}
JSObject* obj = NewDateObjectMsec(context(), t);
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(), 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(), 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(), 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_DATA_VIEW_OBJECT: {
// readDataView adds the array to allObjs.
return readDataView(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;
}
case SCTAG_SAVED_FRAME_OBJECT: {
auto obj = readSavedFrame(data);
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(), 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, JS_SCERR_TRANSFERABLE);
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
// DiscardTransferables.
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;
}
JSObject*
JSStructuredCloneReader::readSavedFrame(uint32_t principalsTag)
{
RootedSavedFrame savedFrame(context(), SavedFrame::create(context()));
if (!savedFrame)
return nullptr;
JSPrincipals* principals;
if (principalsTag == SCTAG_JSPRINCIPALS) {
if (!context()->runtime()->readPrincipals) {
JS_ReportErrorNumber(context(), GetErrorMessage, nullptr,
JSMSG_SC_UNSUPPORTED_TYPE);
return nullptr;
}
if (!context()->runtime()->readPrincipals(context(), this, &principals))
return nullptr;
} else if (principalsTag == SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM) {
principals = &ReconstructedSavedFramePrincipals::IsSystem;
principals->refcount++;
} else if (principalsTag == SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM) {
principals = &ReconstructedSavedFramePrincipals::IsNotSystem;
principals->refcount++;
} else if (principalsTag == SCTAG_NULL_JSPRINCIPALS) {
principals = nullptr;
} else {
JS_ReportErrorNumber(context(), GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "bad SavedFrame principals");
return nullptr;
}
savedFrame->initPrincipalsAlreadyHeld(principals);
RootedValue source(context());
if (!startRead(&source) || !source.isString())
return nullptr;
auto atomSource = AtomizeString(context(), source.toString());
if (!atomSource)
return nullptr;
savedFrame->initSource(atomSource);
RootedValue lineVal(context());
uint32_t line;
if (!startRead(&lineVal) || !lineVal.isNumber() || !ToUint32(context(), lineVal, &line))
return nullptr;
savedFrame->initLine(line);
RootedValue columnVal(context());
uint32_t column;
if (!startRead(&columnVal) || !columnVal.isNumber() || !ToUint32(context(), columnVal, &column))
return nullptr;
savedFrame->initColumn(column);
RootedValue name(context());
if (!startRead(&name) || !(name.isString() || name.isNull()))
return nullptr;
JSAtom* atomName = nullptr;
if (name.isString()) {
atomName = AtomizeString(context(), name.toString());
if (!atomName)
return nullptr;
}
savedFrame->initFunctionDisplayName(atomName);
RootedValue cause(context());
if (!startRead(&cause) || !(cause.isString() || cause.isNull()))
return nullptr;
JSAtom* atomCause = nullptr;
if (cause.isString()) {
atomCause = AtomizeString(context(), cause.toString());
if (!atomCause)
return nullptr;
}
savedFrame->initAsyncCause(atomCause);
return savedFrame;
}
// Perform the whole recursive reading procedure.
bool
JSStructuredCloneReader::read(MutableHandleValue vp)
{
if (!readTransferMap())
return false;
// Start out by reading in the main object and pushing it onto the 'objs'
// stack. The data related to this object and its descendants extends from
// here to the SCTAG_END_OF_KEYS at the end of the stream.
if (!startRead(vp))
return false;
// Stop when the stack shows that all objects have been read.
while (objs.length() != 0) {
// What happens depends on the top obj on the objs stack.
RootedObject obj(context(), &objs.back().toObject());
uint32_t tag, data;
if (!in.getPair(&tag, &data))
return false;
if (tag == SCTAG_END_OF_KEYS) {
// Pop the current obj off the stack, since we are done with it and
// its children.
MOZ_ALWAYS_TRUE(in.readPair(&tag, &data));
objs.popBack();
continue;
}
// The input stream contains a sequence of "child" values, whose
// interpretation depends on the type of obj. These values can be
// anything, and startRead() will push onto 'objs' for any non-leaf
// value (i.e., anything that may contain children).
//
// startRead() will allocate the (empty) object, but note that when
// startRead() returns, 'key' is not yet initialized with any of its
// properties. Those will be filled in by returning to the head of this
// loop, processing the first child obj, and continuing until all
// children have been fully created.
//
// Note that this means the ordering in the stream is a little funky
// for things like Map. See the comment above startWrite() for an
// example.
RootedValue key(context());
if (!startRead(&key))
return false;
if (key.isNull() &&
!(obj->is<MapObject>() || obj->is<SetObject>() || obj->is<SavedFrame>()))
{
// Backwards compatibility: Null formerly indicated the end of
// object properties.
objs.popBack();
continue;
}
// Set object: the values between obj header (from startRead()) and
// SCTAG_END_OF_KEYS are all interpreted as values to add to the set.
if (obj->is<SetObject>()) {
if (!SetObject::add(context(), obj, key))
return false;
continue;
}
// SavedFrame object: there is one following value, the parent
// SavedFrame, which is either null or another SavedFrame object.
if (obj->is<SavedFrame>()) {
SavedFrame* parentFrame;
if (key.isNull())
parentFrame = nullptr;
else if (key.isObject() && key.toObject().is<SavedFrame>())
parentFrame = &key.toObject().as<SavedFrame>();
else
return false;
obj->as<SavedFrame>().initParent(parentFrame);
continue;
}
// Everything else uses a series of key,value,key,value,... Value
// objects.
RootedValue val(context());
if (!startRead(&val))
return false;
if (obj->is<MapObject>()) {
// For a Map, store those <key,value> pairs in the contained map
// data structure.
if (!MapObject::set(context(), obj, key, val))
return false;
} else {
// For any other Object, interpret them as plain properties.
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, GetErrorMessage, nullptr, JSMSG_SC_BAD_CLONE_VERSION);
return false;
}
const JSStructuredCloneCallbacks* callbacks = optionalCallbacks;
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;
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, bool freeData)
{
DiscardTransferables(data, nbytes, optionalCallbacks, closure);
if (freeData) {
js_free(data);
}
return true;
}
JS_PUBLIC_API(bool)
JS_StructuredCloneHasTransferables(const uint64_t* data, size_t nbytes,
bool* hasTransferable)
{
*hasTransferable = StructuredCloneHasTransferObjects(data, nbytes);
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;
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)
{
ownTransferables_ = other.ownTransferables_;
other.steal(&data_, &nbytes_, &version_, &callbacks_, &closure_);
}
JSAutoStructuredCloneBuffer&
JSAutoStructuredCloneBuffer::operator=(JSAutoStructuredCloneBuffer&& other)
{
MOZ_ASSERT(&other != this);
clear();
ownTransferables_ = other.ownTransferables_;
other.steal(&data_, &nbytes_, &version_, &callbacks_, &closure_);
return *this;
}
void
JSAutoStructuredCloneBuffer::clear(const JSStructuredCloneCallbacks* optionalCallbacks,
void* optionalClosure)
{
if (!data_)
return;
const JSStructuredCloneCallbacks* callbacks =
optionalCallbacks ? optionalCallbacks : callbacks_;
void* closure = optionalClosure ? optionalClosure : closure_;
if (ownTransferables_ == OwnsTransferablesIfAny)
DiscardTransferables(data_, nbytes_, callbacks, closure);
ownTransferables_ = NoTransferables;
js_free(data_);
data_ = nullptr;
nbytes_ = 0;
version_ = 0;
}
bool
JSAutoStructuredCloneBuffer::copy(const uint64_t* srcData, size_t nbytes, uint32_t version,
const JSStructuredCloneCallbacks* callbacks,
void* closure)
{
// transferable objects cannot be copied
if (StructuredCloneHasTransferObjects(data_, nbytes_))
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;
callbacks_ = callbacks;
closure_ = closure;
ownTransferables_ = NoTransferables;
return true;
}
void
JSAutoStructuredCloneBuffer::adopt(uint64_t* data, size_t nbytes, uint32_t version,
const JSStructuredCloneCallbacks* callbacks,
void* closure)
{
clear();
data_ = data;
nbytes_ = nbytes;
version_ = version;
callbacks_ = callbacks;
closure_ = closure;
ownTransferables_ = OwnsTransferablesIfAny;
}
void
JSAutoStructuredCloneBuffer::steal(uint64_t** datap, size_t* nbytesp, uint32_t* versionp,
const JSStructuredCloneCallbacks** callbacks,
void** closure)
{
*datap = data_;
*nbytesp = nbytes_;
if (versionp)
*versionp = version_;
if (callbacks)
*callbacks = callbacks_;
if (closure)
*closure = closure_;
data_ = nullptr;
nbytes_ = 0;
version_ = 0;
callbacks_ = 0;
closure_ = 0;
ownTransferables_ = NoTransferables;
}
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) {
ownTransferables_ = OwnsTransferablesIfAny;
} else {
data_ = nullptr;
nbytes_ = 0;
version_ = JS_STRUCTURED_CLONE_VERSION;
ownTransferables_ = NoTransferables;
}
return ok;
}
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(), 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);
}