Source code

Revision control

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* 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 data algorithms of
*
* The spec is in two parts:
*
* - StructuredSerialize examines a JS value and produces a graph of Records.
* - StructuredDeserialize walks the Records and produces a new JS value.
*
* The differences between our implementation and the spec are minor:
*
* - We call the two phases "write" and "read".
* - Our algorithms use an explicit work stack, rather than recursion.
* - Serialized data is a flat array of bytes, not a (possibly cyclic) graph
* of "Records".
* - As a consequence, we handle non-treelike object graphs differently.
* We serialize objects that appear in multiple places in the input as
* backreferences, using sequential integer indexes.
* See `JSStructuredCloneReader::allObjs`, our take on the "memory" map
* in the spec's StructuredDeserialize.
*/
#include "js/StructuredClone.h"
#include "mozilla/Casting.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/RangedPtr.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Unused.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "jsapi.h"
#include "jsdate.h"
#include "builtin/DataViewObject.h"
#include "builtin/MapObject.h"
#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
#include "js/ArrayBuffer.h" // JS::{ArrayBufferHasData,DetachArrayBuffer,IsArrayBufferObject,New{,Mapped}ArrayBufferWithContents,ReleaseMappedArrayBufferContents}
#include "js/Date.h"
#include "js/experimental/TypedData.h" // JS_NewDataView, JS_New{{Ui,I}nt{8,16,32},Float{32,64},Uint8Clamped,Big{Ui,I}nt64}ArrayWithBuffer
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/GCHashTable.h"
#include "js/Object.h" // JS::GetBuiltinClass
#include "js/RegExpFlags.h" // JS::RegExpFlag, JS::RegExpFlags
#include "js/ScalarType.h" // js::Scalar::Type
#include "js/SharedArrayBuffer.h" // JS::IsSharedArrayBufferObject
#include "js/Wrapper.h"
#include "vm/BigIntType.h"
#include "vm/JSContext.h"
#include "vm/PlainObject.h" // js::PlainObject
#include "vm/RegExpObject.h"
#include "vm/SavedFrame.h"
#include "vm/SharedArrayObject.h"
#include "vm/TypedArrayObject.h"
#include "vm/WrapperObject.h"
#include "wasm/WasmJS.h"
#include "vm/InlineCharBuffer-inl.h"
#include "vm/JSContext-inl.h"
#include "vm/JSObject-inl.h"
using namespace js;
using JS::CanonicalizeNaN;
using JS::GetBuiltinClass;
using JS::RegExpFlag;
using JS::RegExpFlags;
using JS::RootedValueVector;
using mozilla::AssertedCast;
using mozilla::BitwiseCast;
using mozilla::NativeEndian;
using mozilla::NumbersAreIdentical;
using mozilla::RangedPtr;
// 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_HEADER = 0xFFF10000,
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_V2, // Old version, for backwards compatibility.
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_V2, // Old version, for backwards compatibility.
SCTAG_MAP_OBJECT,
SCTAG_SET_OBJECT,
SCTAG_END_OF_KEYS,
SCTAG_DO_NOT_USE_3, // Required for backwards compatibility
SCTAG_DATA_VIEW_OBJECT_V2, // Old version, for backwards compatibility.
SCTAG_SAVED_FRAME_OBJECT,
// No new tags before principals.
SCTAG_JSPRINCIPALS,
SCTAG_NULL_JSPRINCIPALS,
SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_SYSTEM,
SCTAG_RECONSTRUCTED_SAVED_FRAME_PRINCIPALS_IS_NOT_SYSTEM,
SCTAG_SHARED_ARRAY_BUFFER_OBJECT,
SCTAG_SHARED_WASM_MEMORY_OBJECT,
SCTAG_BIGINT,
SCTAG_BIGINT_OBJECT,
SCTAG_ARRAY_BUFFER_OBJECT,
SCTAG_TYPED_ARRAY_OBJECT,
SCTAG_DATA_VIEW_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,
// BigInt64 and BigUint64 are not supported in the v1 format.
SCTAG_TYPED_ARRAY_V1_MAX = SCTAG_TYPED_ARRAY_V1_UINT8_CLAMPED,
// 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_STORED_ARRAY_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 {
template <typename T, typename AllocPolicy>
struct BufferIterator {
using BufferList = mozilla::BufferList<AllocPolicy>;
explicit BufferIterator(const BufferList& buffer)
: mBuffer(buffer), mIter(buffer.Iter()) {
static_assert(8 % sizeof(T) == 0);
}
explicit BufferIterator(const JSStructuredCloneData& data)
: mBuffer(data.bufList_), mIter(data.Start()) {}
BufferIterator& operator=(const BufferIterator& other) {
MOZ_ASSERT(&mBuffer == &other.mBuffer);
mIter = other.mIter;
return *this;
}
[[nodiscard]] bool advance(size_t size = sizeof(T)) {
return mIter.AdvanceAcrossSegments(mBuffer, size);
}
BufferIterator operator++(int) {
BufferIterator ret = *this;
if (!advance(sizeof(T))) {
MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete");
}
return ret;
}
BufferIterator& operator+=(size_t size) {
if (!advance(size)) {
MOZ_ASSERT(false, "Failed to read StructuredCloneData. Data incomplete");
}
return *this;
}
size_t operator-(const BufferIterator& other) {
MOZ_ASSERT(&mBuffer == &other.mBuffer);
return mBuffer.RangeLength(other.mIter, mIter);
}
bool done() const { return mIter.Done(); }
[[nodiscard]] bool readBytes(char* outData, size_t size) {
return mBuffer.ReadBytes(mIter, outData, size);
}
void write(const T& data) {
MOZ_ASSERT(mIter.HasRoomFor(sizeof(T)));
*reinterpret_cast<T*>(mIter.Data()) = data;
}
T peek() const {
MOZ_ASSERT(mIter.HasRoomFor(sizeof(T)));
return *reinterpret_cast<T*>(mIter.Data());
}
bool canPeek() const { return mIter.HasRoomFor(sizeof(T)); }
const BufferList& mBuffer;
typename BufferList::IterImpl mIter;
};
SharedArrayRawBufferRefs& SharedArrayRawBufferRefs::operator=(
SharedArrayRawBufferRefs&& other) {
takeOwnership(std::move(other));
return *this;
}
SharedArrayRawBufferRefs::~SharedArrayRawBufferRefs() { releaseAll(); }
bool SharedArrayRawBufferRefs::acquire(JSContext* cx,
SharedArrayRawBuffer* rawbuf) {
if (!refs_.append(rawbuf)) {
ReportOutOfMemory(cx);
return false;
}
if (!rawbuf->addReference()) {
refs_.popBack();
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_SC_SAB_REFCNT_OFLO);
return false;
}
return true;
}
bool SharedArrayRawBufferRefs::acquireAll(
JSContext* cx, const SharedArrayRawBufferRefs& that) {
if (!refs_.reserve(refs_.length() + that.refs_.length())) {
ReportOutOfMemory(cx);
return false;
}
for (auto ref : that.refs_) {
if (!ref->addReference()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_SC_SAB_REFCNT_OFLO);
return false;
}
MOZ_ALWAYS_TRUE(refs_.append(ref));
}
return true;
}
void SharedArrayRawBufferRefs::takeOwnership(SharedArrayRawBufferRefs&& other) {
MOZ_ASSERT(refs_.empty());
refs_ = std::move(other.refs_);
}
void SharedArrayRawBufferRefs::releaseAll() {
for (auto ref : refs_) {
ref->dropReference();
}
refs_.clear();
}
// SCOutput provides an interface to write raw data -- eg uint64_ts, doubles,
// arrays of bytes -- into a structured clone data output stream. It also knows
// how to free any transferable data within that stream.
//
// Note that it contains a full JSStructuredCloneData object, which holds the
// callbacks necessary to read/write/transfer/free the data. For the purpose of
// this class, only the freeTransfer callback is relevant; the rest of the
// callbacks are used by the higher-level JSStructuredCloneWriter interface.
struct SCOutput {
public:
using Iter = BufferIterator<uint64_t, SystemAllocPolicy>;
SCOutput(JSContext* cx, JS::StructuredCloneScope scope);
JSContext* context() const { return cx; }
JS::StructuredCloneScope scope() const { return buf.scope(); }
void sameProcessScopeRequired() { buf.sameProcessScopeRequired(); }
[[nodiscard]] bool write(uint64_t u);
[[nodiscard]] bool writePair(uint32_t tag, uint32_t data);
[[nodiscard]] bool writeDouble(double d);
[[nodiscard]] bool writeBytes(const void* p, size_t nbytes);
[[nodiscard]] bool writeChars(const Latin1Char* p, size_t nchars);
[[nodiscard]] bool writeChars(const char16_t* p, size_t nchars);
template <class T>
[[nodiscard]] bool writeArray(const T* p, size_t nelems);
void setCallbacks(const JSStructuredCloneCallbacks* callbacks, void* closure,
OwnTransferablePolicy policy) {
buf.setCallbacks(callbacks, closure, policy);
}
void extractBuffer(JSStructuredCloneData* data) { *data = std::move(buf); }
void discardTransferables();
uint64_t tell() const { return buf.Size(); }
uint64_t count() const { return buf.Size() / sizeof(uint64_t); }
Iter iter() { return Iter(buf); }
size_t offset(Iter dest) { return dest - iter(); }
JSContext* cx;
JSStructuredCloneData buf;
};
class SCInput {
typedef js::BufferIterator<uint64_t, SystemAllocPolicy> BufferIterator;
public:
SCInput(JSContext* cx, const JSStructuredCloneData& data);
JSContext* context() const { return cx; }
static void getPtr(uint64_t data, void** ptr);
static void getPair(uint64_t data, uint32_t* tagp, uint32_t* datap);
[[nodiscard]] bool read(uint64_t* p);
[[nodiscard]] bool readPair(uint32_t* tagp, uint32_t* datap);
[[nodiscard]] bool readDouble(double* p);
[[nodiscard]] bool readBytes(void* p, size_t nbytes);
[[nodiscard]] bool readChars(Latin1Char* p, size_t nchars);
[[nodiscard]] bool readChars(char16_t* p, size_t nchars);
[[nodiscard]] bool readPtr(void**);
[[nodiscard]] bool get(uint64_t* p);
[[nodiscard]] bool getPair(uint32_t* tagp, uint32_t* datap);
const BufferIterator& tell() const { return point; }
void seekTo(const BufferIterator& pos) { point = pos; }
[[nodiscard]] bool seekBy(size_t pos) {
if (!point.advance(pos)) {
reportTruncated();
return false;
}
return true;
}
template <class T>
[[nodiscard]] bool readArray(T* p, size_t nelems);
bool reportTruncated() {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_SC_BAD_SERIALIZED_DATA, "truncated");
return false;
}
private:
void staticAssertions() {
static_assert(sizeof(char16_t) == 2);
static_assert(sizeof(uint32_t) == 4);
}
JSContext* cx;
BufferIterator point;
};
} // namespace js
struct JSStructuredCloneReader {
public:
explicit JSStructuredCloneReader(SCInput& in, JS::StructuredCloneScope scope,
const JS::CloneDataPolicy& cloneDataPolicy,
const JSStructuredCloneCallbacks* cb,
void* cbClosure)
: in(in),
allowedScope(scope),
cloneDataPolicy(cloneDataPolicy),
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 readHeader();
bool readTransferMap();
template <typename CharT>
JSString* readStringImpl(uint32_t nchars, gc::InitialHeap heap);
JSString* readString(uint32_t data, gc::InitialHeap heap = gc::DefaultHeap);
BigInt* readBigInt(uint32_t data);
[[nodiscard]] bool readTypedArray(uint32_t arrayType, uint64_t nelems,
MutableHandleValue vp, bool v1Read = false);
[[nodiscard]] bool readDataView(uint64_t byteLength, MutableHandleValue vp);
[[nodiscard]] bool readArrayBuffer(StructuredDataType type, uint32_t data,
MutableHandleValue vp);
[[nodiscard]] bool readSharedArrayBuffer(MutableHandleValue vp);
[[nodiscard]] bool readSharedWasmMemory(uint32_t nbytes,
MutableHandleValue vp);
[[nodiscard]] bool readV1ArrayBuffer(uint32_t arrayType, uint32_t nelems,
MutableHandleValue vp);
JSObject* readSavedFrame(uint32_t principalsTag);
[[nodiscard]] bool startRead(MutableHandleValue vp,
gc::InitialHeap strHeap = gc::DefaultHeap);
SCInput& in;
// The widest scope that the caller will accept, where
// SameProcess is the widest (it can store anything it wants)
// and DifferentProcess is the narrowest (it cannot contain pointers and must
// be valid cross-process.)
JS::StructuredCloneScope allowedScope;
const JS::CloneDataPolicy cloneDataPolicy;
// Stack of objects with properties remaining to be read.
RootedValueVector objs;
// Array of all objects read during this deserialization, for resolving
// backreferences.
//
// For backreferences to work correctly, objects must be added to this
// array in exactly the order expected by the version of the Writer that
// created the serialized data, even across years and format versions. This
// is usually no problem, since both algorithms do a single linear pass
// over the serialized data. There is one hitch; see readTypedArray.
//
// The values in this vector are objects, except it can temporarily have
// one `undefined` placeholder value (the readTypedArray hack).
RootedValueVector 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,
JS::StructuredCloneScope scope,
const JS::CloneDataPolicy& cloneDataPolicy,
const JSStructuredCloneCallbacks* cb,
void* cbClosure, const Value& tVal)
: out(cx, scope),
callbacks(cb),
closure(cbClosure),
objs(out.context()),
counts(out.context()),
objectEntries(out.context()),
otherEntries(out.context()),
memory(out.context()),
transferable(out.context(), tVal),
transferableObjects(out.context(), TransferableObjectsSet(cx)),
cloneDataPolicy(cloneDataPolicy) {
out.setCallbacks(cb, cbClosure, OwnTransferablePolicy::NoTransferables);
}
~JSStructuredCloneWriter();
bool init() {
return parseTransferable() && writeHeader() && writeTransferMap();
}
bool write(HandleValue v);
SCOutput& output() { return out; }
void extractBuffer(JSStructuredCloneData* newData) {
out.extractBuffer(newData);
}
private:
JSStructuredCloneWriter() = delete;
JSStructuredCloneWriter(const JSStructuredCloneWriter&) = delete;
JSContext* context() { return out.context(); }
bool writeHeader();
bool writeTransferMap();
bool writeString(uint32_t tag, JSString* str);
bool writeBigInt(uint32_t tag, BigInt* bi);
bool writeArrayBuffer(HandleObject obj);
bool writeTypedArray(HandleObject obj);
bool writeDataView(HandleObject obj);
bool writeSharedArrayBuffer(HandleObject obj);
bool writeSharedWasmMemory(HandleObject obj);
bool startObject(HandleObject obj, bool* backref);
bool startWrite(HandleValue v);
bool traverseObject(HandleObject obj, ESClass cls);
bool traverseMap(HandleObject obj);
bool traverseSet(HandleObject obj);
bool traverseSavedFrame(HandleObject obj);
template <typename... Args>
bool reportDataCloneError(uint32_t errorId, Args&&... aArgs);
bool parseTransferable();
bool transferOwnership();
inline void checkStack();
SCOutput out;
// The user defined callbacks that will be used to signal cloning, in some
// cases.
const JSStructuredCloneCallbacks* callbacks;
// Any value passed to the callbacks.
void* closure;
// 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.
RootedValueVector 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
RootedIdVector objectEntries;
// For Map: Key followed by value
// For Set: Key
// For SavedFrame: parent SavedFrame
RootedValueVector otherEntries;
// 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*>,
SystemAllocPolicy>;
Rooted<CloneMemory> memory;
struct TransferableObjectsHasher : public DefaultHasher<JSObject*> {
static inline HashNumber hash(const Lookup& l) {
return DefaultHasher<JSObject*>::hash(l);
}
};
// Set of transferable objects
RootedValue transferable;
typedef GCHashSet<JSObject*, TransferableObjectsHasher>
TransferableObjectsSet;
Rooted<TransferableObjectsSet> transferableObjects;
const JS::CloneDataPolicy cloneDataPolicy;
friend bool JS_WriteString(JSStructuredCloneWriter* w, HandleString str);
friend bool JS_WriteTypedArray(JSStructuredCloneWriter* w, HandleValue v);
friend bool JS_ObjectNotWritten(JSStructuredCloneWriter* w, HandleObject obj);
};
JS_FRIEND_API uint64_t js::GetSCOffset(JSStructuredCloneWriter* writer) {
MOZ_ASSERT(writer);
return writer->output().count() * sizeof(uint64_t);
}
static_assert(SCTAG_END_OF_BUILTIN_TYPES <= JS_SCTAG_USER_MIN);
static_assert(JS_SCTAG_USER_MIN <= JS_SCTAG_USER_MAX);
static_assert(Scalar::Int8 == 0);
template <typename... Args>
static void ReportDataCloneError(JSContext* cx,
const JSStructuredCloneCallbacks* callbacks,
uint32_t errorId, void* closure,
Args&&... aArgs) {
unsigned errorNumber;
switch (errorId) {
case JS_SCERR_DUP_TRANSFERABLE:
errorNumber = JSMSG_SC_DUP_TRANSFERABLE;
break;
case JS_SCERR_TRANSFERABLE:
errorNumber = JSMSG_SC_NOT_TRANSFERABLE;
break;
case JS_SCERR_UNSUPPORTED_TYPE:
errorNumber = JSMSG_SC_UNSUPPORTED_TYPE;
break;
case JS_SCERR_SHMEM_TRANSFERABLE:
errorNumber = JSMSG_SC_SHMEM_TRANSFERABLE;
break;
case JS_SCERR_TYPED_ARRAY_DETACHED:
errorNumber = JSMSG_TYPED_ARRAY_DETACHED;
break;
case JS_SCERR_WASM_NO_TRANSFER:
errorNumber = JSMSG_WASM_NO_TRANSFER;
break;
case JS_SCERR_NOT_CLONABLE:
errorNumber = JSMSG_SC_NOT_CLONABLE;
break;
case JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP:
errorNumber = JSMSG_SC_NOT_CLONABLE_WITH_COOP_COEP;
break;
default:
MOZ_CRASH("Unkown errorId");
break;
}
if (callbacks && callbacks->reportError) {
MOZ_RELEASE_ASSERT(!cx->isExceptionPending());
JSErrorReport report;
report.errorNumber = errorNumber;
// Get js error message if it's possible and propagate it through callback.
if (JS_ExpandErrorArgumentsASCII(cx, GetErrorMessage, errorNumber, &report,
std::forward<Args>(aArgs)...) &&
report.message()) {
callbacks->reportError(cx, errorId, closure, report.message().c_str());
} else {
ReportOutOfMemory(cx);
callbacks->reportError(cx, errorId, closure, "");
}
return;
}
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, errorNumber,
std::forward<Args>(aArgs)...);
}
bool WriteStructuredClone(JSContext* cx, HandleValue v,
JSStructuredCloneData* bufp,
JS::StructuredCloneScope scope,
const JS::CloneDataPolicy& cloneDataPolicy,
const JSStructuredCloneCallbacks* cb, void* cbClosure,
const Value& transferable) {
JSStructuredCloneWriter w(cx, scope, cloneDataPolicy, cb, cbClosure,
transferable);
if (!w.init()) {
return false;
}
if (!w.write(v)) {
return false;
}
w.extractBuffer(bufp);
return true;
}
bool ReadStructuredClone(JSContext* cx, const JSStructuredCloneData& data,
JS::StructuredCloneScope scope, MutableHandleValue vp,
const JS::CloneDataPolicy& cloneDataPolicy,
const JSStructuredCloneCallbacks* cb,
void* cbClosure) {
SCInput in(cx, data);
JSStructuredCloneReader r(in, scope, cloneDataPolicy, cb, cbClosure);
return r.read(vp);
}
static bool StructuredCloneHasTransferObjects(
const JSStructuredCloneData& data) {
if (data.Size() < sizeof(uint64_t)) {
return false;
}
uint64_t u;
BufferIterator<uint64_t, SystemAllocPolicy> iter(data);
MOZ_ALWAYS_TRUE(iter.readBytes(reinterpret_cast<char*>(&u), sizeof(u)));
uint32_t tag = uint32_t(u >> 32);
return (tag == SCTAG_TRANSFER_MAP_HEADER);
}
namespace js {
SCInput::SCInput(JSContext* cx, const JSStructuredCloneData& data)
: cx(cx), point(data) {
static_assert(JSStructuredCloneData::BufferList::kSegmentAlignment % 8 == 0,
"structured clone buffer reads should be aligned");
MOZ_ASSERT(data.Size() % 8 == 0);
}
bool SCInput::read(uint64_t* p) {
if (!point.canPeek()) {
*p = 0; // initialize to shut GCC up
return reportTruncated();
}
*p = NativeEndian::swapFromLittleEndian(point.peek());
MOZ_ALWAYS_TRUE(point.advance());
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.canPeek()) {
return reportTruncated();
}
*p = NativeEndian::swapFromLittleEndian(point.peek());
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(uint64_t data, uint32_t* tagp, uint32_t* datap) {
uint64_t u = NativeEndian::swapFromLittleEndian(data);
*tagp = uint32_t(u >> 32);
*datap = uint32_t(u);
}
bool SCInput::readDouble(double* p) {
uint64_t u;
if (!read(&u)) {
return false;
}
*p = CanonicalizeNaN(mozilla::BitwiseCast<double>(u));
return true;
}
template <typename T>
static void swapFromLittleEndianInPlace(T* ptr, size_t nelems) {
if (nelems > 0) {
NativeEndian::swapFromLittleEndianInPlace(ptr, nelems);
}
}
template <>
void swapFromLittleEndianInPlace(uint8_t* ptr, size_t nelems) {}
// Data is packed into an integral number of uint64_t words. Compute the
// padding required to finish off the final word.
static size_t ComputePadding(size_t nelems, size_t elemSize) {
// We want total length mod 8, where total length is nelems * sizeof(T),
// but that might overflow. So reduce nelems to nelems mod 8, since we are
// going to be doing a mod 8 later anyway.
size_t leftoverLength = (nelems % sizeof(uint64_t)) * elemSize;
return (-leftoverLength) & (sizeof(uint64_t) - 1);
}
template <class T>
bool SCInput::readArray(T* p, size_t nelems) {
if (!nelems) {
return true;
}
static_assert(sizeof(uint64_t) % sizeof(T) == 0);
// Fail if nelems is so huge that computing the full size will overflow.
mozilla::CheckedInt<size_t> size =
mozilla::CheckedInt<size_t>(nelems) * sizeof(T);
if (!size.isValid()) {
return reportTruncated();
}
if (!point.readBytes(reinterpret_cast<char*>(p), size.value())) {
// To avoid any way in which uninitialized data could escape, zero the array
// if filling it failed.
std::uninitialized_fill_n(p, nelems, 0);
return false;
}
swapFromLittleEndianInPlace(p, nelems);
point += ComputePadding(nelems, sizeof(T));
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(uint64_t data, void** ptr) {
*ptr = reinterpret_cast<void*>(NativeEndian::swapFromLittleEndian(data));
}
bool SCInput::readPtr(void** p) {
uint64_t u;
if (!read(&u)) {
return false;
}
*p = reinterpret_cast<void*>(u);
return true;
}
SCOutput::SCOutput(JSContext* cx, JS::StructuredCloneScope scope)
: cx(cx), buf(scope) {}
bool SCOutput::write(uint64_t u) {
uint64_t v = NativeEndian::swapToLittleEndian(u);
if (!buf.AppendBytes(reinterpret_cast<char*>(&v), sizeof(u))) {
ReportOutOfMemory(context());
return false;
}
return true;
}
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 <class T>
bool SCOutput::writeArray(const T* p, size_t nelems) {
static_assert(8 % sizeof(T) == 0);
static_assert(sizeof(uint64_t) % sizeof(T) == 0);
if (nelems == 0) {
return true;
}
for (size_t i = 0; i < nelems; i++) {
T value = NativeEndian::swapToLittleEndian(p[i]);
if (!buf.AppendBytes(reinterpret_cast<char*>(&value), sizeof(value))) {
return false;
}
}
// Zero-pad to 8 bytes boundary.
size_t padbytes = ComputePadding(nelems, sizeof(T));
char zeroes[sizeof(uint64_t)] = {0};
if (!buf.AppendBytes(zeroes, padbytes)) {
return false;
}
return true;
}
template <>
bool SCOutput::writeArray<uint8_t>(const uint8_t* p, size_t nelems) {
if (nelems == 0) {
return true;
}
if (!buf.AppendBytes(reinterpret_cast<const char*>(p), nelems)) {
return false;
}
// zero-pad to 8 bytes boundary
size_t padbytes = ComputePadding(nelems, 1);
char zeroes[sizeof(uint64_t)] = {0};
if (!buf.AppendBytes(zeroes, padbytes)) {
return false;
}
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);
}
void SCOutput::discardTransferables() { buf.discardTransferables(); }
} // namespace js
// If the buffer contains Transferables, free them. Note that custom
// Transferables will use the JSStructuredCloneCallbacks::freeTransfer() to
// delete their transferables.
void JSStructuredCloneData::discardTransferables() {
if (!Size()) {
return;
}
if (ownTransferables_ != OwnTransferablePolicy::OwnsTransferablesIfAny) {
return;
}
// DifferentProcess clones cannot contain pointers, so nothing needs to be
// released.
if (scope() == JS::StructuredCloneScope::DifferentProcess) {
return;
}
FreeTransferStructuredCloneOp freeTransfer = nullptr;
if (callbacks_) {
freeTransfer = callbacks_->freeTransfer;
}
auto point = BufferIterator<uint64_t, SystemAllocPolicy>(*this);
if (point.done()) {
return; // Empty buffer
}
uint32_t tag, data;
MOZ_RELEASE_ASSERT(point.canPeek());
SCInput::getPair(point.peek(), &tag, &data);
MOZ_ALWAYS_TRUE(point.advance());
if (tag == SCTAG_HEADER) {
if (point.done()) {
return;
}
MOZ_RELEASE_ASSERT(point.canPeek());
SCInput::getPair(point.peek(), &tag, &data);
MOZ_ALWAYS_TRUE(point.advance());
}
if (tag != SCTAG_TRANSFER_MAP_HEADER) {
return;
}
if (TransferableMapHeader(data) == SCTAG_TM_TRANSFERRED) {
return;
}
// freeTransfer should not GC
JS::AutoSuppressGCAnalysis nogc;
if (point.done()) {
return;
}
MOZ_RELEASE_ASSERT(point.canPeek());
uint64_t numTransferables = NativeEndian::swapFromLittleEndian(point.peek());
MOZ_ALWAYS_TRUE(point.advance());
while (numTransferables--) {
if (!point.canPeek()) {
return;
}
uint32_t ownership;
SCInput::getPair(point.peek(), &tag, &ownership);
MOZ_ALWAYS_TRUE(point.advance());
MOZ_ASSERT(tag >= SCTAG_TRANSFER_MAP_PENDING_ENTRY);
if (!point.canPeek()) {
return;
}
void* content;
SCInput::getPtr(point.peek(), &content);
MOZ_ALWAYS_TRUE(point.advance());
if (!point.canPeek()) {
return;
}
uint64_t extraData = NativeEndian::swapFromLittleEndian(point.peek());
MOZ_ALWAYS_TRUE(point.advance());
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 (freeTransfer) {
freeTransfer(tag, JS::TransferableOwnership(ownership), content,
extraData, closure_);
} else {
MOZ_ASSERT(false, "unknown ownership");
}
}
}
static_assert(JSString::MAX_LENGTH < UINT32_MAX);
JSStructuredCloneWriter::~JSStructuredCloneWriter() {
// Free any transferable data left lying around in the buffer
if (out.count()) {
out.discardTransferables();
}
}
bool JSStructuredCloneWriter::parseTransferable() {
// NOTE: The transferables set is tested for non-emptiness at various
// junctures in structured cloning, so this set must be initialized
// by this method in all non-error cases.
MOZ_ASSERT(transferableObjects.empty(),
"parseTransferable called with stale data");
if (transferable.isNull() || transferable.isUndefined()) {
return true;
}
if (!transferable.isObject()) {
return reportDataCloneError(JS_SCERR_TRANSFERABLE);
}
JSContext* cx = context();
RootedObject array(cx, &transferable.toObject());
bool isArray;
if (!JS::IsArrayObject(cx, array, &isArray)) {
return false;
}
if (!isArray) {
return reportDataCloneError(JS_SCERR_TRANSFERABLE);
}
uint32_t length;
if (!JS::GetArrayLength(cx, array, &length)) {
return false;
}
// Initialize the set for the provided array's length.
if (!transferableObjects.reserve(length)) {
return false;
}
if (length == 0) {
return true;
}
RootedValue v(context());
RootedObject tObj(context());
for (uint32_t i = 0; i < length; ++i) {
if (!CheckForInterrupt(cx)) {
return false;
}
if (!JS_GetElement(cx, array, i, &v)) {
return false;
}
if (!v.isObject()) {
return reportDataCloneError(JS_SCERR_TRANSFERABLE);
}
tObj = &v.toObject();
RootedObject unwrappedObj(cx, CheckedUnwrapStatic(tObj));
if (!unwrappedObj) {
ReportAccessDenied(cx);
return false;
}
// Shared memory cannot be transferred because it is not possible (nor
// desirable) to detach the memory in agents that already hold a
// reference to it.
if (unwrappedObj->is<SharedArrayBufferObject>()) {
return reportDataCloneError(JS_SCERR_SHMEM_TRANSFERABLE);
}
else if (unwrappedObj->is<WasmMemoryObject>()) {
if (unwrappedObj->as<WasmMemoryObject>().isShared()) {
return reportDataCloneError(JS_SCERR_SHMEM_TRANSFERABLE);
}
}
// External array buffers may be able to be transferred in the future,
// but that is not currently implemented.
else if (unwrappedObj->is<ArrayBufferObject>()) {
if (unwrappedObj->as<ArrayBufferObject>().isExternal()) {
return reportDataCloneError(JS_SCERR_TRANSFERABLE);
}
}
else {
if (!out.buf.callbacks_ || !out.buf.callbacks_->canTransfer) {
return reportDataCloneError(JS_SCERR_TRANSFERABLE);
}
JSAutoRealm ar(cx, unwrappedObj);
bool sameProcessScopeRequired = false;
if (!out.buf.callbacks_->canTransfer(
cx, unwrappedObj, &sameProcessScopeRequired, out.buf.closure_)) {
return false;
}
if (sameProcessScopeRequired) {
output().sameProcessScopeRequired();
}
}
// No duplicates allowed
auto p = transferableObjects.lookupForAdd(tObj);
if (p) {
return reportDataCloneError(JS_SCERR_DUP_TRANSFERABLE);
}
if (!transferableObjects.add(p, tObj)) {
return false;
}
}
return true;
}
template <typename... Args>
bool JSStructuredCloneWriter::reportDataCloneError(uint32_t errorId,
Args&&... aArgs) {
ReportDataCloneError(context(), out.buf.callbacks_, errorId, out.buf.closure_,
std::forward<Args>(aArgs)...);
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);
}
bool JSStructuredCloneWriter::writeBigInt(uint32_t tag, BigInt* bi) {
bool signBit = bi->isNegative();
size_t length = bi->digitLength();
// The length must fit in 31 bits to leave room for a sign bit.
if (length > size_t(INT32_MAX)) {
return false;
}
uint32_t lengthAndSign = length | (static_cast<uint32_t>(signBit) << 31);
if (!out.writePair(tag, lengthAndSign)) {
return false;
}
return out.writeArray(bi->digits().data(), 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 = std::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 == objectEntries.length() + otherEntries.length());
} else {
MOZ_ASSERT(total <= objectEntries.length() + otherEntries.length());
}
size_t j = objs.length();
for (size_t i = 0; i < limit; i++) {
--j;
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(),
obj->maybeUnwrapAs<TypedArrayObject>());
JSAutoRealm ar(context(), tarr);
if (!TypedArrayObject::ensureHasBuffer(context(), tarr)) {
return false;
}
if (!out.writePair(SCTAG_TYPED_ARRAY_OBJECT, uint32_t(tarr->type()))) {
return false;
}
uint64_t nelems = tarr->length().get();
if (!out.write(nelems)) {
return false;
}
// Write out the ArrayBuffer tag and contents
RootedValue val(context(), tarr->bufferValue());
if (!startWrite(val)) {
return false;
}
uint64_t byteOffset = tarr->byteOffset().get();
return out.write(byteOffset);
}
bool JSStructuredCloneWriter::writeDataView(HandleObject obj) {
Rooted<DataViewObject*> view(context(), obj->maybeUnwrapAs<DataViewObject>());
JSAutoRealm ar(context(), view);
if (!out.writePair(SCTAG_DATA_VIEW_OBJECT, 0)) {
return false;
}
uint64_t byteLength = view->byteLength().get();
if (!out.write(byteLength)) {
return false;
}
// Write out the ArrayBuffer tag and contents
RootedValue val(context(), view->bufferValue());
if (!startWrite(val)) {
return false;
}
uint64_t byteOffset = view->byteOffset().get();
return out.write(byteOffset);
}
bool JSStructuredCloneWriter::writeArrayBuffer(HandleObject obj) {
Rooted<ArrayBufferObject*> buffer(context(),
obj->maybeUnwrapAs<ArrayBufferObject>());
JSAutoRealm ar(context(), buffer);
if (!out.writePair(SCTAG_ARRAY_BUFFER_OBJECT, 0)) {
return false;
}
uint64_t byteLength = buffer->byteLength().get();
if (!out.write(byteLength)) {
return false;
}
return out.writeBytes(buffer->dataPointer(), byteLength);
}
bool JSStructuredCloneWriter::writeSharedArrayBuffer(HandleObject obj) {
MOZ_ASSERT(obj->canUnwrapAs<SharedArrayBufferObject>());
if (!cloneDataPolicy.areSharedMemoryObjectsAllowed()) {
auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled()
? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP
: JS_SCERR_NOT_CLONABLE;
reportDataCloneError(error, "SharedArrayBuffer");
return false;
}
output().sameProcessScopeRequired();
// We must not transmit SAB pointers (including for WebAssembly.Memory)
// cross-process. The cloneDataPolicy should have guarded against this;
// since it did not then throw, with a very explicit message.
if (output().scope() > JS::StructuredCloneScope::SameProcess) {
JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
JSMSG_SC_SHMEM_POLICY);
return false;
}
Rooted<SharedArrayBufferObject*> sharedArrayBuffer(
context(), obj->maybeUnwrapAs<SharedArrayBufferObject>());
SharedArrayRawBuffer* rawbuf = sharedArrayBuffer->rawBufferObject();
if (!out.buf.refsHeld_.acquire(context(), rawbuf)) {
return false;
}
// We must serialize the length so that the buffer object arrives in the
// receiver with the same length, and not with the length read from the
// rawbuf - that length can be different, and it can change at any time.
intptr_t p = reinterpret_cast<intptr_t>(rawbuf);
uint64_t byteLength = sharedArrayBuffer->byteLength().get();
if (!(out.writePair(SCTAG_SHARED_ARRAY_BUFFER_OBJECT,
static_cast<uint32_t>(sizeof(p))) &&
out.writeBytes(&byteLength, sizeof(byteLength)) &&
out.writeBytes(&p, sizeof(p)))) {
return false;
}
if (callbacks && callbacks->sabCloned &&
!callbacks->sabCloned(context(), /*receiving=*/false, closure)) {
return false;
}
return true;
}
bool JSStructuredCloneWriter::writeSharedWasmMemory(HandleObject obj) {
MOZ_ASSERT(obj->canUnwrapAs<WasmMemoryObject>());
// Check the policy here so that we can report a sane error.
if (!cloneDataPolicy.areSharedMemoryObjectsAllowed()) {
auto error = context()->realm()->creationOptions().getCoopAndCoepEnabled()
? JS_SCERR_NOT_CLONABLE_WITH_COOP_COEP
: JS_SCERR_NOT_CLONABLE;
reportDataCloneError(error, "WebAssembly.Memory");
return false;
}
// If this changes, might need to change what we write.
MOZ_ASSERT(WasmMemoryObject::RESERVED_SLOTS == 2);
Rooted<WasmMemoryObject*> memoryObj(context(),
&obj->unwrapAs<WasmMemoryObject>());
Rooted<SharedArrayBufferObject*> sab(
context(), &memoryObj->buffer().as<SharedArrayBufferObject>());
return out.writePair(SCTAG_SHARED_WASM_MEMORY_OBJECT, 0) &&
writeSharedArrayBuffer(sab);
}
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())) {
ReportOutOfMemory(context());
return false;
}
if (memory.count() == UINT32_MAX) {
JS_ReportErrorNumberASCII(context(), GetErrorMessage, nullptr,
JSMSG_NEED_DIET, "object graph to serialize");
return false;
}
return true;
}
static bool TryAppendNativeProperties(JSContext* cx, HandleObject obj,
MutableHandleIdVector entries,
size_t* properties, bool* optimized) {
*optimized = false;
if (!obj->is<NativeObject>()) {
return true;
}
HandleNativeObject nobj = obj.as<NativeObject>();
if (nobj->isIndexed() || nobj->is<TypedArrayObject>() ||
nobj->getClass()->getNewEnumerate() || nobj->getClass()->getEnumerate()) {
return true;
}
*optimized = true;
size_t count = 0;
// We iterate from the last to the first shape, so the property names
// are already in reverse order.
RootedShape shape(cx, nobj->lastProperty());
for (Shape::Range<NoGC> r(shape); !r.empty(); r.popFront()) {
jsid id = r.front().propidRaw();
// Ignore symbols and non-enumerable properties.
if (!r.front().enumerable() || JSID_IS_SYMBOL(id)) {
continue;
}
MOZ_ASSERT(JSID_IS_STRING(id));
if (!entries.append(id)) {
return false;
}
count++;
}
// Add dense element ids in reverse order.
for (uint32_t i = nobj->getDenseInitializedLength(); i > 0; --i) {
if (nobj->getDenseElement(i - 1).isMagic(JS_ELEMENTS_HOLE)) {
continue;
}
if (!entries.append(INT_TO_JSID(i - 1))) {
return false;
}
count++;
}
*properties = count;
return true;
}
// 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
//
// obj1 = { key1: { key1.1: val1.1, key1.2: val1.2 }, key2: {} }
//
// would be stored as:
//
// <Object tag for obj1>
// <key1 data>
// <Object tag for key1's value>
// <key1.1 data>
// <val1.1 data>
// <key1.2 data>
// <val1.2 data>
// <end-of-children marker for key1's value>
// <key2 data>
// <Object tag for key2's value>
// <end-of-children marker for key2's value>
// <end-of-children marker for obj1>
//
// This nests nicely (ie, an entire recursive value starts with its tag and
// ends with its end-of-children marker) and so it can be presented indented.
// But see traverseMap below for how this looks different for Maps.
bool JSStructuredCloneWriter::traverseObject(HandleObject obj, ESClass cls) {
size_t count;
bool optimized = false;
if (!TryAppendNativeProperties(context(), obj, &objectEntries, &count,
&optimized)) {
return false;
}
if (!optimized) {
// Get enumerable property ids and put them in reverse order so that they
// will come off the stack in forward order.
RootedIdVector properties(context());
if (!GetPropertyKeys(context(), obj, JSITER_OWNONLY, &properties)) {
return false;
}
for (size_t i = properties.length(); i > 0; --i) {
jsid id = properties[i - 1];
MOZ_ASSERT(JSID_IS_STRING(id) || JSID_IS_INT(id));
if (!objectEntries.append(id)) {
return false;
}
}
count = properties.length();
}
// Push obj and count to the stack.
if (!objs.append(ObjectValue(*obj)) || !counts.append(count)) {
return false;
}
checkStack();
#if DEBUG
ESClass cls2;
if (!GetBuiltinClass(context(), obj, &cls2)) {
return false;
}
MOZ_ASSERT(cls2 == cls);
#endif
// Write the header for obj.
if (cls == ESClass::Array) {
uint32_t length = 0;
if (!JS::GetArrayLength(context(), obj, &length)) {
return false;
}
return out.writePair(SCTAG_ARRAY_OBJECT,
NativeEndian::swapToLittleEndian(length));
}
return out.writePair(SCTAG_OBJECT_OBJECT, 0);
}
// Use the same basic setup as for traverseObject, but now keys can themselves
// be complex objects. Keys and values are visited first via startWrite(), then
// the key's children (if any) are handled, then the value's children.
//
// m = new Map();
// m.set(key1 = ..., value1 = ...)
//
// where key1 and value2 are both objects would be stored as
//
// <Map tag>
// <key1 class tag>
// <value1 class tag>
// ...key1 data...
// <end-of-children marker for key1>
// ...value1 data...
// <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::traverseMap(HandleObject obj) {
Rooted<GCVector<Value>> newEntries(context(), GCVector<Value>(context()));
{
// If there is no wrapper, the compartment munging is a no-op.
RootedObject unwrapped(context(), obj->maybeUnwrapAs<MapObject>());
MOZ_ASSERT(unwrapped);
JSAutoRealm ar(context(), unwrapped);
if (!MapObject::getKeysAndValuesInterleaved(unwrapped, &newEntries)) {
return false;
}
}
if (!context()->compartment()->wrap(context(), &newEntries)) {
return false;
}
for (size_t i = newEntries.length(); i > 0; --i) {
if (!otherEntries.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);
}
// Similar to traverseMap, only there is a single value instead of a key and
// value, and thus no interleaving is possible: a value will be fully emitted
// before the next value is begun.
bool JSStructuredCloneWriter::traverseSet(HandleObject obj) {
Rooted<GCVector<Value>> keys(context(), GCVector<Value>(context()));
{
// If there is no wrapper, the compartment munging is a no-op.
RootedObject unwrapped(context(), obj->maybeUnwrapAs<SetObject>());
MOZ_ASSERT(unwrapped);
JSAutoRealm ar(context(), unwrapped);
if (!SetObject::keys(context(), unwrapped, &keys)) {
return false;
}
}
if (!context()->compartment()->wrap(context(), &keys)) {
return false;
}
for (size_t i = keys.