blob: 6869515c7d794b8bd0f29bd292930067410298a1 [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/. */
#include "mozilla/Endian.h"
/*
* 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 "jsclone.h"
#include "mozilla/FloatingPoint.h"
#include "jsdate.h"
#include "jstypedarray.h"
#include "jstypedarrayinlines.h"
#include "vm/BooleanObject-inl.h"
#include "vm/RegExpObject-inl.h"
using namespace js;
using mozilla::IsNaN;
using mozilla::LittleEndian;
using mozilla::NativeEndian;
using JS::CanonicalizeNaN;
enum StructuredDataType {
/* Structured data types provided by the engine */
SCTAG_FLOAT_MAX = 0xFFF00000,
SCTAG_NULL = 0xFFFF0000,
SCTAG_UNDEFINED,
SCTAG_BOOLEAN,
SCTAG_INDEX,
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_TRANSFER_MAP_HEADER,
SCTAG_TRANSFER_MAP,
SCTAG_TYPED_ARRAY_OBJECT,
SCTAG_TYPED_ARRAY_V1_MIN = 0xFFFF0100,
SCTAG_TYPED_ARRAY_V1_INT8 = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_INT8,
SCTAG_TYPED_ARRAY_V1_UINT8 = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_UINT8,
SCTAG_TYPED_ARRAY_V1_INT16 = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_INT16,
SCTAG_TYPED_ARRAY_V1_UINT16 = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_UINT16,
SCTAG_TYPED_ARRAY_V1_INT32 = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_INT32,
SCTAG_TYPED_ARRAY_V1_UINT32 = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_UINT32,
SCTAG_TYPED_ARRAY_V1_FLOAT32 = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_FLOAT32,
SCTAG_TYPED_ARRAY_V1_FLOAT64 = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_FLOAT64,
SCTAG_TYPED_ARRAY_V1_UINT8_CLAMPED = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_UINT8_CLAMPED,
SCTAG_TYPED_ARRAY_V1_MAX = SCTAG_TYPED_ARRAY_V1_MIN + TypedArray::TYPE_MAX - 1,
SCTAG_END_OF_BUILTIN_TYPES
};
enum TransferableMapHeader {
SCTAG_TM_NOT_MARKED = 0,
SCTAG_TM_MARKED
};
JS_FRIEND_API(uint64_t)
js_GetSCOffset(JSStructuredCloneWriter* writer)
{
JS_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(TypedArray::TYPE_INT8 == 0);
bool
js::WriteStructuredClone(JSContext *cx, HandleValue v, uint64_t **bufp, size_t *nbytesp,
const JSStructuredCloneCallbacks *cb, void *cbClosure,
jsval transferable)
{
SCOutput out(cx);
JSStructuredCloneWriter w(out, cb, cbClosure, transferable);
return w.init() && w.write(v) && out.extractBuffer(bufp, nbytesp);
}
bool
js::ReadStructuredClone(JSContext *cx, uint64_t *data, size_t nbytes, Value *vp,
const JSStructuredCloneCallbacks *cb, void *cbClosure)
{
SCInput in(cx, data, nbytes);
/* XXX disallow callers from using internal pointers to GC things. */
SkipRoot skip(cx, &in);
JSStructuredCloneReader r(in, cb, cbClosure);
return r.read(vp);
}
bool
js::ClearStructuredClone(const uint64_t *data, size_t nbytes)
{
const uint64_t *point = data;
const uint64_t *end = data + nbytes / 8;
uint64_t u = LittleEndian::readUint64(point++);
uint32_t tag = uint32_t(u >> 32);
if (tag == SCTAG_TRANSFER_MAP_HEADER) {
if ((TransferableMapHeader)uint32_t(u) == SCTAG_TM_NOT_MARKED) {
while (point != end) {
uint64_t u = LittleEndian::readUint64(point++);
uint32_t tag = uint32_t(u >> 32);
if (tag == SCTAG_TRANSFER_MAP) {
u = LittleEndian::readUint64(point++);
js_free(reinterpret_cast<void*>(
static_cast<uintptr_t>(u)));
} else {
// The only things in the transfer map should be
// SCTAG_TRANSFER_MAP tags paired with pointers. If we find
// any other tag, we've walked off the end of the transfer
// map.
break;
}
}
}
}
js_free((void *)data);
return true;
}
bool
js::StructuredCloneHasTransferObjects(const uint64_t *data, size_t nbytes, bool *hasTransferable)
{
*hasTransferable = false;
if (data) {
uint64_t u = LittleEndian::readUint64(data);
uint32_t tag = uint32_t(u >> 32);
if (tag == SCTAG_TRANSFER_MAP_HEADER) {
*hasTransferable = true;
}
}
return true;
}
static inline uint64_t
PairToUInt64(uint32_t tag, uint32_t data)
{
return uint64_t(data) | (uint64_t(tag) << 32);
}
bool
SCInput::eof()
{
JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_SC_BAD_SERIALIZED_DATA, "truncated");
return false;
}
SCInput::SCInput(JSContext *cx, uint64_t *data, size_t nbytes)
: cx(cx), point(data), end(data + nbytes / 8)
{
JS_ASSERT((uintptr_t(data) & 7) == 0);
JS_ASSERT((nbytes & 7) == 0);
}
bool
SCInput::read(uint64_t *p)
{
if (point == end) {
*p = 0; /* initialize to shut GCC up */
return eof();
}
*p = LittleEndian::readUint64(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 == end)
return eof();
*p = LittleEndian::readUint64(point);
return true;
}
bool
SCInput::getPair(uint32_t *tagp, uint32_t *datap)
{
uint64_t u;
if (!get(&u))
return false;
*tagp = uint32_t(u >> 32);
*datap = uint32_t(u);
return true;
}
bool
SCInput::replace(uint64_t u)
{
if (point == end)
return eof();
LittleEndian::writeUint64(point, u);
return true;
}
bool
SCInput::replacePair(uint32_t tag, uint32_t data)
{
return replace(PairToUInt64(tag, data));
}
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)
{
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(end - point))
return eof();
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(jschar *p, size_t nchars)
{
JS_ASSERT(sizeof(jschar) == sizeof(uint16_t));
return readArray((uint16_t *) p, nchars);
}
bool
SCInput::readPtr(void **p)
{
// On a 32 bit system the void* variable we have to write to is only
// 32 bits, so we create a 64 temporary and discard the unused bits.
uint64_t tmp;
bool ret = read(&tmp);
#if defined(STARBOARD)
*p = reinterpret_cast<void*>(static_cast<uintptr_t>(tmp));
#else
*p = reinterpret_cast<void*>(tmp);
#endif
return ret;
}
SCOutput::SCOutput(JSContext *cx) : cx(cx), buf(cx) {}
bool
SCOutput::write(uint64_t u)
{
return buf.append(NativeEndian::swapToLittleEndian(u));
}
bool
SCOutput::writePair(uint32_t tag, uint32_t data)
{
/*
* As it happens, the tag word appears after the data word in the output.
* This is because exponents occupy the last 2 bytes of doubles on the
* little-endian platforms we care most about.
*
* For example, JSVAL_TRUE is written using writePair(SCTAG_BOOLEAN, 1).
* PairToUInt64 produces the number 0xFFFF000200000001.
* That is written out as the bytes 01 00 00 00 02 00 FF FF.
*/
return write(PairToUInt64(tag, data));
}
static inline uint64_t
ReinterpretDoubleAsUInt64(double d)
{
union {
double d;
uint64_t u;
} pun;
pun.d = d;
return pun.u;
}
static inline double
ReinterpretUInt64AsDouble(uint64_t u)
{
union {
uint64_t u;
double d;
} pun;
pun.u = u;
return pun.d;
}
static inline double
ReinterpretPairAsDouble(uint32_t tag, uint32_t data)
{
return ReinterpretUInt64AsDouble(PairToUInt64(tag, data));
}
bool
SCOutput::writeDouble(double d)
{
return write(ReinterpretDoubleAsUInt64(CanonicalizeNaN(d)));
}
template <typename T>
static void
copyAndSwapToLittleEndian(void *dest, const T *src, size_t nelems)
{
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)
{
JS_ASSERT(8 % sizeof(T) == 0);
JS_ASSERT(sizeof(uint64_t) % sizeof(T) == 0);
if (nelems == 0)
return true;
if (nelems + sizeof(uint64_t) / sizeof(T) - 1 < nelems) {
js_ReportAllocationOverflow(context());
return false;
}
size_t nwords = JS_HOWMANY(nelems, sizeof(uint64_t) / sizeof(T));
size_t start = buf.length();
if (!buf.growByUninitialized(nwords))
return false;
buf.back() = 0; /* zero-pad to an 8-byte boundary */
T *q = (T *) &buf[start];
copyAndSwapToLittleEndian(q, p, nelems);
return true;
}
bool
SCOutput::writeBytes(const void *p, size_t nbytes)
{
return writeArray((const uint8_t *) p, nbytes);
}
bool
SCOutput::writeChars(const jschar *p, size_t nchars)
{
JS_ASSERT(sizeof(jschar) == sizeof(uint16_t));
return writeArray((const uint16_t *) 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()) != NULL;
}
JS_STATIC_ASSERT(JSString::MAX_LENGTH < UINT32_MAX);
bool
JSStructuredCloneWriter::parseTransferable()
{
transferableObjects.clear();
if (JSVAL_IS_NULL(transferable) || JSVAL_IS_VOID(transferable))
return true;
if (!transferable.isObject()) {
reportErrorTransferable();
return false;
}
RootedObject array(context(), &transferable.toObject());
if (!JS_IsArrayObject(context(), array)) {
reportErrorTransferable();
return false;
}
uint32_t length;
if (!JS_GetArrayLength(context(), array, &length)) {
return false;
}
RootedValue v(context());
for (uint32_t i = 0; i < length; ++i) {
if (!JS_GetElement(context(), array, i, v.address())) {
return false;
}
if (!v.isObject()) {
reportErrorTransferable();
return false;
}
JSObject* tObj = CheckedUnwrap(&v.toObject());
if (!tObj) {
JS_ReportError(context(), "Permission denied to access object");
return false;
}
if (!tObj->is<ArrayBufferObject>()) {
reportErrorTransferable();
return false;
}
// No duplicate:
if (transferableObjects.has(tObj)) {
reportErrorTransferable();
return false;
}
if (!transferableObjects.putNew(tObj))
return false;
}
return true;
}
void
JSStructuredCloneWriter::reportErrorTransferable()
{
if (callbacks && callbacks->reportError)
return callbacks->reportError(context(), JS_SCERR_TRANSFERABLE);
}
bool
JSStructuredCloneWriter::writeString(uint32_t tag, JSString *str)
{
size_t length = str->length();
const jschar *chars = str->getChars(context());
if (!chars)
return false;
return out.writePair(tag, uint32_t(length)) && out.writeChars(chars, length);
}
bool
JSStructuredCloneWriter::writeId(jsid id)
{
if (JSID_IS_INT(id))
return out.writePair(SCTAG_INDEX, uint32_t(JSID_TO_INT(id)));
JS_ASSERT(JSID_IS_STRING(id));
return writeString(SCTAG_STRING, JSID_TO_STRING(id));
}
inline void
JSStructuredCloneWriter::checkStack()
{
#ifdef DEBUG
/* To avoid making serialization O(n^2), limit stack-checking at 10. */
const size_t MAX = 10;
size_t limit = Min(counts.length(), MAX);
JS_ASSERT(objs.length() == counts.length());
size_t total = 0;
for (size_t i = 0; i < limit; i++) {
JS_ASSERT(total + counts[i] >= total);
total += counts[i];
}
if (counts.length() <= MAX)
JS_ASSERT(total == ids.length());
else
JS_ASSERT(total <= ids.length());
size_t j = objs.length();
for (size_t i = 0; i < limit; i++)
JS_ASSERT(memory.has(&objs[--j].toObject()));
#endif
}
JS_PUBLIC_API(JSBool)
JS_WriteTypedArray(JSStructuredCloneWriter *w, jsval v)
{
JS_ASSERT(v.isObject());
assertSameCompartment(w->context(), v);
RootedObject obj(w->context(), &v.toObject());
// If the object is a security wrapper, see if we're allowed to unwrap it.
// If we aren't, throw.
if (obj->isWrapper())
obj = CheckedUnwrap(obj);
if (!obj) {
JS_ReportError(w->context(), "Permission denied to access object");
return false;
}
return w->writeTypedArray(obj);
}
/*
* 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 arr)
{
if (!out.writePair(SCTAG_TYPED_ARRAY_OBJECT, TypedArray::length(arr)))
return false;
uint64_t type = TypedArray::type(arr);
if (!out.write(type))
return false;
// Write out the ArrayBuffer tag and contents
if (!startWrite(TypedArray::bufferValue(arr)))
return false;
return out.write(TypedArray::byteOffset(arr));
}
bool
JSStructuredCloneWriter::writeArrayBuffer(HandleObject obj)
{
ArrayBufferObject &buffer = obj->as<ArrayBufferObject>();
return out.writePair(SCTAG_ARRAY_BUFFER_OBJECT, buffer.byteLength()) &&
out.writeBytes(buffer.dataPointer(), buffer.byteLength());
}
bool
JSStructuredCloneWriter::startObject(HandleObject obj, bool *backref)
{
/* Handle cycles in the object graph. */
CloneMemory::AddPtr p = memory.lookupForAdd(obj);
if ((*backref = p))
return out.writePair(SCTAG_BACK_REFERENCE_OBJECT, p->value);
if (!memory.add(p, obj, memory.count()))
return false;
if (memory.count() == UINT32_MAX) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL,
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.
*/
size_t initialLength = ids.length();
if (!GetPropertyNames(context(), obj, JSITER_OWNONLY, &ids))
return false;
jsid *begin = ids.begin() + initialLength, *end = ids.end();
size_t count = size_t(end - begin);
Reverse(begin, end);
/* Push obj and count to the stack. */
if (!objs.append(ObjectValue(*obj)) || !counts.append(count))
return false;
checkStack();
/* Write the header for obj. */
return out.writePair(obj->isArray() ? SCTAG_ARRAY_OBJECT : SCTAG_OBJECT_OBJECT, 0);
}
bool
JSStructuredCloneWriter::startWrite(const Value &v)
{
assertSameCompartment(context(), v);
if (v.isString()) {
return writeString(SCTAG_STRING, v.toString());
} else if (v.isNumber()) {
return out.writeDouble(v.toNumber());
} else if (v.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());
// The object might be a security wrapper. See if we can clone what's
// behind it. If we can, unwrap the object.
obj = CheckedUnwrap(obj);
if (!obj) {
JS_ReportError(context(), "Permission denied to access object");
return false;
}
AutoCompartment ac(context(), obj);
bool backref;
if (!startObject(obj, &backref))
return false;
if (backref)
return true;
if (obj->is<RegExpObject>()) {
RegExpObject &reobj = obj->as<RegExpObject>();
return out.writePair(SCTAG_REGEXP_OBJECT, reobj.getFlags()) &&
writeString(SCTAG_STRING, reobj.getSource());
} else if (obj->isDate()) {
double d = js_DateGetMsecSinceEpoch(obj);
return out.writePair(SCTAG_DATE_OBJECT, 0) && out.writeDouble(d);
} else if (obj->isTypedArray()) {
return writeTypedArray(obj);
} else if (obj->is<ArrayBufferObject>() && obj->as<ArrayBufferObject>().hasData()) {
return writeArrayBuffer(obj);
} else if (obj->isObject() || obj->isArray()) {
return traverseObject(obj);
} else if (obj->is<BooleanObject>()) {
return out.writePair(SCTAG_BOOLEAN_OBJECT, obj->as<BooleanObject>().unbox());
} else if (obj->is<NumberObject>()) {
return out.writePair(SCTAG_NUMBER_OBJECT, 0) &&
out.writeDouble(obj->as<NumberObject>().unbox());
} else if (obj->is<StringObject>()) {
return writeString(SCTAG_STRING_OBJECT, obj->as<StringObject>().unbox());
}
if (callbacks && callbacks->write)
return callbacks->write(context(), this, obj, closure);
/* else fall through */
}
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL, JSMSG_SC_UNSUPPORTED_TYPE);
return false;
}
bool
JSStructuredCloneWriter::writeTransferMap()
{
if (!transferableObjects.empty()) {
if (!out.writePair(SCTAG_TRANSFER_MAP_HEADER, (uint32_t)SCTAG_TM_NOT_MARKED))
return false;
for (HashSet<JSObject*>::Range r = transferableObjects.all();
!r.empty(); r.popFront()) {
JSObject *obj = r.front();
if (!memory.put(obj, memory.count()))
return false;
void *content;
uint8_t *data;
if (!JS_StealArrayBufferContents(context(), obj, &content, &data))
return false;
if (!out.writePair(SCTAG_TRANSFER_MAP, 0) || !out.writePtr(content))
return false;
}
}
return true;
}
bool
JSStructuredCloneWriter::write(const Value &v)
{
if (!startWrite(v))
return false;
while (!counts.empty()) {
RootedObject obj(context(), &objs.back().toObject());
AutoCompartment ac(context(), obj);
if (counts.back()) {
counts.back()--;
RootedId id(context(), ids.back());
ids.popBack();
checkStack();
if (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.
*/
RootedObject obj2(context());
RootedShape prop(context());
if (!HasOwnProperty<CanGC>(context(), obj->getOps()->lookupGeneric, obj, id,
&obj2, &prop)) {
return false;
}
if (prop) {
RootedValue val(context());
if (!writeId(id) ||
!JSObject::getGeneric(context(), obj, obj, id, &val) ||
!startWrite(val))
return false;
}
}
} else {
out.writePair(SCTAG_NULL, 0);
objs.popBack();
counts.popBack();
}
}
memory.clear();
return true;
}
bool
JSStructuredCloneReader::checkDouble(double d)
{
jsval_layout l;
l.asDouble = d;
if (!JSVAL_IS_DOUBLE_IMPL(l)) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL,
JSMSG_SC_BAD_SERIALIZED_DATA, "unrecognized NaN");
return false;
}
return true;
}
class Chars {
JSContext *cx;
jschar *p;
public:
Chars(JSContext *cx) : cx(cx), p(NULL) {}
~Chars() { if (p) js_free(p); }
bool allocate(size_t len) {
JS_ASSERT(!p);
// We're going to null-terminate!
p = cx->pod_malloc<jschar>(len + 1);
if (p) {
p[len] = jschar(0);
return true;
}
return false;
}
jschar *get() { return p; }
void forget() { p = NULL; }
};
JSString *
JSStructuredCloneReader::readString(uint32_t nchars)
{
if (nchars > JSString::MAX_LENGTH) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL, JSMSG_SC_BAD_SERIALIZED_DATA,
"string length");
return NULL;
}
Chars chars(context());
if (!chars.allocate(nchars) || !in.readChars(chars.get(), nchars))
return NULL;
JSString *str = js_NewString<CanGC>(context(), chars.get(), nchars);
if (str)
chars.forget();
return str;
}
static uint32_t
TagToV1ArrayType(uint32_t tag)
{
JS_ASSERT(tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX);
return tag - SCTAG_TYPED_ARRAY_V1_MIN;
}
JS_PUBLIC_API(JSBool)
JS_ReadTypedArray(JSStructuredCloneReader *r, jsval *vp)
{
uint32_t tag, nelems;
if (!r->input().readPair(&tag, &nelems))
return false;
if (tag >= SCTAG_TYPED_ARRAY_V1_MIN && tag <= SCTAG_TYPED_ARRAY_V1_MAX) {
return r->readTypedArray(TagToV1ArrayType(tag), nelems, vp, true);
} else if (tag == SCTAG_TYPED_ARRAY_OBJECT) {
uint64_t arrayType;
if (!r->input().read(&arrayType))
return false;
return r->readTypedArray(arrayType, nelems, vp);
} else {
JS_ReportErrorNumber(r->context(), js_GetErrorMessage, NULL,
JSMSG_SC_BAD_SERIALIZED_DATA, "expected type array");
return false;
}
}
bool
JSStructuredCloneReader::readTypedArray(uint32_t arrayType, uint32_t nelems, Value *vp,
bool v1Read)
{
if (arrayType > TypedArray::TYPE_UINT8_CLAMPED) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL,
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 = JSVAL_NULL;
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.address()))
return false;
byteOffset = 0;
} else {
if (!startRead(v.address()))
return false;
uint64_t n;
if (!in.read(&n))
return false;
byteOffset = n;
}
RootedObject buffer(context(), &v.toObject());
RootedObject obj(context(), NULL);
switch (arrayType) {
case TypedArray::TYPE_INT8:
obj = JS_NewInt8ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case TypedArray::TYPE_UINT8:
obj = JS_NewUint8ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case TypedArray::TYPE_INT16:
obj = JS_NewInt16ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case TypedArray::TYPE_UINT16:
obj = JS_NewUint16ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case TypedArray::TYPE_INT32:
obj = JS_NewInt32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case TypedArray::TYPE_UINT32:
obj = JS_NewUint32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case TypedArray::TYPE_FLOAT32:
obj = JS_NewFloat32ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case TypedArray::TYPE_FLOAT64:
obj = JS_NewFloat64ArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
case TypedArray::TYPE_UINT8_CLAMPED:
obj = JS_NewUint8ClampedArrayWithBuffer(context(), buffer, byteOffset, nelems);
break;
default:
JS_NOT_REACHED("unknown TypedArray type");
return false;
}
if (!obj)
return false;
vp->setObject(*obj);
allObjs[placeholderIndex] = *vp;
return true;
}
bool
JSStructuredCloneReader::readArrayBuffer(uint32_t nbytes, Value *vp)
{
JSObject *obj = ArrayBufferObject::create(context(), nbytes);
if (!obj)
return false;
vp->setObject(*obj);
ArrayBufferObject &buffer = obj->as<ArrayBufferObject>();
JS_ASSERT(buffer.byteLength() == nbytes);
return in.readArray(buffer.dataPointer(), nbytes);
}
static size_t
bytesPerTypedArrayElement(uint32_t arrayType)
{
switch (arrayType) {
case TypedArray::TYPE_INT8:
case TypedArray::TYPE_UINT8:
case TypedArray::TYPE_UINT8_CLAMPED:
return sizeof(uint8_t);
case TypedArray::TYPE_INT16:
case TypedArray::TYPE_UINT16:
return sizeof(uint16_t);
case TypedArray::TYPE_INT32:
case TypedArray::TYPE_UINT32:
case TypedArray::TYPE_FLOAT32:
return sizeof(uint32_t);
case TypedArray::TYPE_FLOAT64:
return sizeof(uint64_t);
default:
JS_NOT_REACHED("unknown TypedArray type");
return 0;
}
}
/*
* 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, Value *vp)
{
JS_ASSERT(arrayType <= TypedArray::TYPE_UINT8_CLAMPED);
uint32_t nbytes = nelems * bytesPerTypedArrayElement(arrayType);
JSObject *obj = ArrayBufferObject::create(context(), nbytes);
if (!obj)
return false;
vp->setObject(*obj);
ArrayBufferObject &buffer = obj->as<ArrayBufferObject>();
JS_ASSERT(buffer.byteLength() == nbytes);
switch (arrayType) {
case TypedArray::TYPE_INT8:
case TypedArray::TYPE_UINT8:
case TypedArray::TYPE_UINT8_CLAMPED:
return in.readArray((uint8_t*) buffer.dataPointer(), nelems);
case TypedArray::TYPE_INT16:
case TypedArray::TYPE_UINT16:
return in.readArray((uint16_t*) buffer.dataPointer(), nelems);
case TypedArray::TYPE_INT32:
case TypedArray::TYPE_UINT32:
case TypedArray::TYPE_FLOAT32:
return in.readArray((uint32_t*) buffer.dataPointer(), nelems);
case TypedArray::TYPE_FLOAT64:
return in.readArray((uint64_t*) buffer.dataPointer(), nelems);
default:
JS_NOT_REACHED("unknown TypedArray type");
return false;
}
}
bool
JSStructuredCloneReader::startRead(Value *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_BOOLEAN:
case SCTAG_BOOLEAN_OBJECT:
vp->setBoolean(!!data);
if (tag == SCTAG_BOOLEAN_OBJECT && !js_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 && !js_PrimitiveToObject(context(), vp))
return false;
break;
}
case SCTAG_NUMBER_OBJECT: {
double d;
if (!in.readDouble(&d) || !checkDouble(d))
return false;
vp->setDouble(d);
if (!js_PrimitiveToObject(context(), vp))
return false;
break;
}
case SCTAG_DATE_OBJECT: {
double d;
if (!in.readDouble(&d) || !checkDouble(d))
return false;
if (!IsNaN(d) && d != TimeClip(d)) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL, JSMSG_SC_BAD_SERIALIZED_DATA,
"date");
return false;
}
JSObject *obj = js_NewDateObjectMsec(context(), d);
if (!obj)
return false;
vp->setObject(*obj);
break;
}
case SCTAG_REGEXP_OBJECT: {
RegExpFlag flags = RegExpFlag(data);
uint32_t tag2, nchars;
if (!in.readPair(&tag2, &nchars))
return false;
if (tag2 != SCTAG_STRING) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL, JSMSG_SC_BAD_SERIALIZED_DATA,
"regexp");
return false;
}
JSString *str = readString(nchars);
if (!str)
return false;
JSStableString *stable = str->ensureStable(context());
if (!stable)
return false;
size_t length = stable->length();
const StableCharPtr chars = stable->chars();
RegExpObject *reobj = RegExpObject::createNoStatics(context(), chars.get(), length, flags, NULL);
if (!reobj)
return false;
vp->setObject(*reobj);
break;
}
case SCTAG_ARRAY_OBJECT:
case SCTAG_OBJECT_OBJECT: {
JSObject *obj = (tag == SCTAG_ARRAY_OBJECT)
? NewDenseEmptyArray(context())
: NewBuiltinClassInstance(context(), &ObjectClass);
if (!obj || !objs.append(ObjectValue(*obj)))
return false;
vp->setObject(*obj);
break;
}
case SCTAG_BACK_REFERENCE_OBJECT: {
if (data >= allObjs.length()) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL,
JSMSG_SC_BAD_SERIALIZED_DATA,
"invalid back reference in input");
return false;
}
*vp = allObjs[data];
return true;
}
case SCTAG_TRANSFER_MAP_HEADER:
// A map header cannot be here but just at the beginning of the buffer.
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL,
JSMSG_SC_BAD_SERIALIZED_DATA,
"invalid input");
return false;
case SCTAG_TRANSFER_MAP:
// A map cannot be here but just at the beginning of the buffer.
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL,
JSMSG_SC_BAD_SERIALIZED_DATA,
"invalid input");
return false;
case SCTAG_ARRAY_BUFFER_OBJECT:
if (!readArrayBuffer(data, vp))
return false;
break;
case SCTAG_TYPED_ARRAY_OBJECT:
// readTypedArray adds the array to allObjs
uint64_t arrayType;
if (!in.read(&arrayType))
return false;
return readTypedArray(arrayType, data, vp);
break;
default: {
if (tag <= SCTAG_FLOAT_MAX) {
double d = ReinterpretPairAsDouble(tag, data);
if (!checkDouble(d))
return false;
vp->setNumber(d);
break;
}
if (SCTAG_TYPED_ARRAY_V1_MIN <= tag && tag <= SCTAG_TYPED_ARRAY_V1_MAX) {
// A v1-format typed array
// readTypedArray adds the array to allObjs
return readTypedArray(TagToV1ArrayType(tag), data, vp, true);
}
if (!callbacks || !callbacks->read) {
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL, 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::readId(jsid *idp)
{
uint32_t tag, data;
if (!in.readPair(&tag, &data))
return false;
if (tag == SCTAG_INDEX) {
*idp = INT_TO_JSID(int32_t(data));
return true;
}
if (tag == SCTAG_STRING) {
JSString *str = readString(data);
if (!str)
return false;
JSAtom *atom = AtomizeString<CanGC>(context(), str);
if (!atom)
return false;
*idp = NON_INTEGER_ATOM_TO_JSID(atom);
return true;
}
if (tag == SCTAG_NULL) {
*idp = JSID_VOID;
return true;
}
JS_ReportErrorNumber(context(), js_GetErrorMessage, NULL, JSMSG_SC_BAD_SERIALIZED_DATA, "id");
return false;
}
bool
JSStructuredCloneReader::readTransferMap()
{
uint32_t tag, data;
if (!in.getPair(&tag, &data))
return false;
if (tag != SCTAG_TRANSFER_MAP_HEADER ||
(TransferableMapHeader)data == SCTAG_TM_MARKED)
return true;
if (!in.replacePair(SCTAG_TRANSFER_MAP_HEADER, SCTAG_TM_MARKED))
return false;
if (!in.readPair(&tag, &data))
return false;
while (1) {
if (!in.getPair(&tag, &data))
return false;
if (tag != SCTAG_TRANSFER_MAP)
break;
void *content;
if (!in.readPair(&tag, &data) || !in.readPtr(&content))
return false;
JSObject *obj = JS_NewArrayBufferWithContents(context(), content);
if (!obj || !allObjs.append(ObjectValue(*obj)))
return false;
}
return true;
}
bool
JSStructuredCloneReader::read(Value *vp)
{
if (!readTransferMap())
return false;
if (!startRead(vp))
return false;
while (objs.length() != 0) {
RootedObject obj(context(), &objs.back().toObject());
RootedId id(context());
if (!readId(id.address()))
return false;
if (JSID_IS_VOID(id)) {
objs.popBack();
} else {
RootedValue v(context());
if (!startRead(v.address()) || !JSObject::defineGeneric(context(), obj, id, v))
return false;
}
}
allObjs.clear();
return true;
}