diff --git a/src/value-serializer.cc b/src/value-serializer.cc index ad7492a2a6..2689fa0bf3 100644 --- a/src/value-serializer.cc +++ b/src/value-serializer.cc @@ -60,6 +60,16 @@ enum class SerializationTag : uint8_t { kBeginJSObject = 'o', // End of a JS object. numProperties:uint32_t kEndJSObject = '{', + // Beginning of a sparse JS array. length:uint32_t + // Elements and properties are written as key/value pairs, like objects. + kBeginSparseJSArray = 'a', + // End of a sparse JS array. numProperties:uint32_t length:uint32_t + kEndSparseJSArray = '@', + // Beginning of a dense JS array. length:uint32_t + // |length| elements, followed by properties as key/value pairs + kBeginDenseJSArray = 'A', + // End of a dense JS array. numProperties:uint32_t length:uint32_t + kEndDenseJSArray = '$', }; ValueSerializer::ValueSerializer(Isolate* isolate) @@ -253,6 +263,8 @@ Maybe ValueSerializer::WriteJSReceiver(Handle receiver) { HandleScope scope(isolate_); switch (instance_type) { + case JS_ARRAY_TYPE: + return WriteJSArray(Handle::cast(receiver)); case JS_OBJECT_TYPE: case JS_API_OBJECT_TYPE: return WriteJSObject(Handle::cast(receiver)); @@ -278,6 +290,67 @@ Maybe ValueSerializer::WriteJSObject(Handle object) { return Just(true); } +Maybe ValueSerializer::WriteJSArray(Handle array) { + uint32_t length; + array->length()->ToArrayLength(&length); + + // To keep things simple, for now we decide between dense and sparse + // serialization based on elements kind. A more principled heuristic could + // count the elements, but would need to take care to note which indices + // existed (as only indices which were enumerable own properties at this point + // should be serialized). + const bool should_serialize_densely = + array->HasFastElements() && !array->HasFastHoleyElements(); + + if (should_serialize_densely) { + // TODO(jbroman): Distinguish between undefined and a hole (this can happen + // if serializing one of the elements deletes another). This requires wire + // format changes. + WriteTag(SerializationTag::kBeginDenseJSArray); + WriteVarint(length); + for (uint32_t i = 0; i < length; i++) { + // Serializing the array's elements can have arbitrary side effects, so we + // cannot rely on still having fast elements, even if it did to begin + // with. + Handle element; + LookupIterator it(isolate_, array, i, array, LookupIterator::OWN); + if (!Object::GetProperty(&it).ToHandle(&element) || + !WriteObject(element).FromMaybe(false)) { + return Nothing(); + } + } + KeyAccumulator accumulator(isolate_, KeyCollectionMode::kOwnOnly, + ENUMERABLE_STRINGS); + if (!accumulator.CollectOwnPropertyNames(array, array).FromMaybe(false)) { + return Nothing(); + } + Handle keys = + accumulator.GetKeys(GetKeysConversion::kConvertToString); + uint32_t properties_written; + if (!WriteJSObjectProperties(array, keys).To(&properties_written)) { + return Nothing(); + } + WriteTag(SerializationTag::kEndDenseJSArray); + WriteVarint(properties_written); + WriteVarint(length); + } else { + WriteTag(SerializationTag::kBeginSparseJSArray); + WriteVarint(length); + Handle keys; + uint32_t properties_written; + if (!KeyAccumulator::GetKeys(array, KeyCollectionMode::kOwnOnly, + ENUMERABLE_STRINGS) + .ToHandle(&keys) || + !WriteJSObjectProperties(array, keys).To(&properties_written)) { + return Nothing(); + } + WriteTag(SerializationTag::kEndSparseJSArray); + WriteVarint(properties_written); + WriteVarint(length); + } + return Just(true); +} + Maybe ValueSerializer::WriteJSObjectProperties( Handle object, Handle keys) { uint32_t properties_written = 0; @@ -454,6 +527,10 @@ MaybeHandle ValueDeserializer::ReadObject() { } case SerializationTag::kBeginJSObject: return ReadJSObject(); + case SerializationTag::kBeginSparseJSArray: + return ReadSparseJSArray(); + case SerializationTag::kBeginDenseJSArray: + return ReadDenseJSArray(); default: return MaybeHandle(); } @@ -517,6 +594,71 @@ MaybeHandle ValueDeserializer::ReadJSObject() { return scope.CloseAndEscape(object); } +MaybeHandle ValueDeserializer::ReadSparseJSArray() { + // If we are at the end of the stack, abort. This function may recurse. + if (StackLimitCheck(isolate_).HasOverflowed()) return MaybeHandle(); + + uint32_t length; + if (!ReadVarint().To(&length)) return MaybeHandle(); + + uint32_t id = next_id_++; + HandleScope scope(isolate_); + Handle array = isolate_->factory()->NewJSArray(0); + JSArray::SetLength(array, length); + AddObjectWithID(id, array); + + uint32_t num_properties; + uint32_t expected_num_properties; + uint32_t expected_length; + if (!ReadJSObjectProperties(array, SerializationTag::kEndSparseJSArray) + .To(&num_properties) || + !ReadVarint().To(&expected_num_properties) || + !ReadVarint().To(&expected_length) || + num_properties != expected_num_properties || length != expected_length) { + return MaybeHandle(); + } + + DCHECK(HasObjectWithID(id)); + return scope.CloseAndEscape(array); +} + +MaybeHandle ValueDeserializer::ReadDenseJSArray() { + // If we are at the end of the stack, abort. This function may recurse. + if (StackLimitCheck(isolate_).HasOverflowed()) return MaybeHandle(); + + uint32_t length; + if (!ReadVarint().To(&length)) return MaybeHandle(); + + uint32_t id = next_id_++; + HandleScope scope(isolate_); + Handle array = isolate_->factory()->NewJSArray( + FAST_HOLEY_ELEMENTS, length, length, INITIALIZE_ARRAY_ELEMENTS_WITH_HOLE); + AddObjectWithID(id, array); + + Handle elements(FixedArray::cast(array->elements()), isolate_); + for (uint32_t i = 0; i < length; i++) { + Handle element; + if (!ReadObject().ToHandle(&element)) return MaybeHandle(); + // TODO(jbroman): Distinguish between undefined and a hole. + if (element->IsUndefined(isolate_)) continue; + elements->set(i, *element); + } + + uint32_t num_properties; + uint32_t expected_num_properties; + uint32_t expected_length; + if (!ReadJSObjectProperties(array, SerializationTag::kEndDenseJSArray) + .To(&num_properties) || + !ReadVarint().To(&expected_num_properties) || + !ReadVarint().To(&expected_length) || + num_properties != expected_num_properties || length != expected_length) { + return MaybeHandle(); + } + + DCHECK(HasObjectWithID(id)); + return scope.CloseAndEscape(array); +} + Maybe ValueDeserializer::ReadJSObjectProperties( Handle object, SerializationTag end_tag) { for (uint32_t num_properties = 0;; num_properties++) { diff --git a/src/value-serializer.h b/src/value-serializer.h index efb5a96753..8de7d56920 100644 --- a/src/value-serializer.h +++ b/src/value-serializer.h @@ -72,6 +72,7 @@ class ValueSerializer { void WriteString(Handle string); Maybe WriteJSReceiver(Handle receiver) WARN_UNUSED_RESULT; Maybe WriteJSObject(Handle object) WARN_UNUSED_RESULT; + Maybe WriteJSArray(Handle array) WARN_UNUSED_RESULT; /* * Reads the specified keys from the object and writes key-value pairs to the @@ -140,6 +141,8 @@ class ValueDeserializer { MaybeHandle ReadUtf8String() WARN_UNUSED_RESULT; MaybeHandle ReadTwoByteString() WARN_UNUSED_RESULT; MaybeHandle ReadJSObject() WARN_UNUSED_RESULT; + MaybeHandle ReadSparseJSArray() WARN_UNUSED_RESULT; + MaybeHandle ReadDenseJSArray() WARN_UNUSED_RESULT; /* * Reads key-value pairs into the object until the specified end tag is diff --git a/test/unittests/value-serializer-unittest.cc b/test/unittests/value-serializer-unittest.cc index d45f374c06..63b50871ff 100644 --- a/test/unittests/value-serializer-unittest.cc +++ b/test/unittests/value-serializer-unittest.cc @@ -707,5 +707,337 @@ TEST_F(ValueSerializerTest, DecodeDictionaryObjectVersion0) { }); } +TEST_F(ValueSerializerTest, RoundTripArray) { + // A simple array of integers. + RoundTripTest("[1, 2, 3, 4, 5]", [this](Local value) { + ASSERT_TRUE(value->IsArray()); + EXPECT_EQ(5, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool( + "Object.getPrototypeOf(result) === Array.prototype")); + EXPECT_TRUE( + EvaluateScriptForResultBool("result.toString() === '1,2,3,4,5'")); + }); + // A long (sparse) array. + RoundTripTest( + "(() => { var x = new Array(1000); x[500] = 42; return x; })()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + EXPECT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[500] === 42")); + }); + // Duplicate reference. + RoundTripTest( + "(() => { var y = {}; return [y, y]; })()", [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === result[1]")); + }); + // Duplicate reference in a sparse array. + RoundTripTest( + "(() => { var x = new Array(1000); x[1] = x[500] = {}; return x; })()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE( + EvaluateScriptForResultBool("typeof result[1] === 'object'")); + EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === result[500]")); + }); + // Self reference. + RoundTripTest( + "(() => { var y = []; y[0] = y; return y; })()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === result")); + }); + // Self reference in a sparse array. + RoundTripTest( + "(() => { var y = new Array(1000); y[519] = y; return y; })()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[519] === result")); + }); + // Array with additional properties. + RoundTripTest( + "(() => { var y = [1, 2]; y.foo = 'bar'; return y; })()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result.toString() === '1,2'")); + EXPECT_TRUE(EvaluateScriptForResultBool("result.foo === 'bar'")); + }); + // Sparse array with additional properties. + RoundTripTest( + "(() => { var y = new Array(1000); y.foo = 'bar'; return y; })()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool( + "result.toString() === ','.repeat(999)")); + EXPECT_TRUE(EvaluateScriptForResultBool("result.foo === 'bar'")); + }); + // The distinction between holes and undefined elements must be maintained. + RoundTripTest("[,undefined]", [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE( + EvaluateScriptForResultBool("typeof result[0] === 'undefined'")); + EXPECT_TRUE( + EvaluateScriptForResultBool("typeof result[1] === 'undefined'")); + EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(0)")); + EXPECT_TRUE(EvaluateScriptForResultBool("result.hasOwnProperty(1)")); + }); +} + +TEST_F(ValueSerializerTest, DecodeArray) { + // A simple array of integers. + DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x41, 0x05, 0x3f, 0x01, 0x49, 0x02, + 0x3f, 0x01, 0x49, 0x04, 0x3f, 0x01, 0x49, 0x06, 0x3f, 0x01, + 0x49, 0x08, 0x3f, 0x01, 0x49, 0x0a, 0x24, 0x00, 0x05, 0x00}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + EXPECT_EQ(5, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool( + "Object.getPrototypeOf(result) === Array.prototype")); + EXPECT_TRUE(EvaluateScriptForResultBool( + "result.toString() === '1,2,3,4,5'")); + }); + // A long (sparse) array. + DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x61, 0xe8, 0x07, 0x3f, 0x01, 0x49, + 0xe8, 0x07, 0x3f, 0x01, 0x49, 0x54, 0x40, 0x01, 0xe8, 0x07}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + EXPECT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[500] === 42")); + }); + // Duplicate reference. + DecodeTest( + {0xff, 0x09, 0x3f, 0x00, 0x41, 0x02, 0x3f, 0x01, 0x6f, 0x7b, 0x00, 0x3f, + 0x02, 0x5e, 0x01, 0x24, 0x00, 0x02}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === result[1]")); + }); + // Duplicate reference in a sparse array. + DecodeTest( + {0xff, 0x09, 0x3f, 0x00, 0x61, 0xe8, 0x07, 0x3f, 0x01, 0x49, + 0x02, 0x3f, 0x01, 0x6f, 0x7b, 0x00, 0x3f, 0x02, 0x49, 0xe8, + 0x07, 0x3f, 0x02, 0x5e, 0x01, 0x40, 0x02, 0xe8, 0x07, 0x00}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE( + EvaluateScriptForResultBool("typeof result[1] === 'object'")); + EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === result[500]")); + }); + // Self reference. + DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x41, 0x01, 0x3f, 0x01, 0x5e, 0x00, 0x24, + 0x00, 0x01, 0x00}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === result")); + }); + // Self reference in a sparse array. + DecodeTest( + {0xff, 0x09, 0x3f, 0x00, 0x61, 0xe8, 0x07, 0x3f, 0x01, 0x49, + 0x8e, 0x08, 0x3f, 0x01, 0x5e, 0x00, 0x40, 0x01, 0xe8, 0x07}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[519] === result")); + }); + // Array with additional properties. + DecodeTest( + {0xff, 0x09, 0x3f, 0x00, 0x41, 0x02, 0x3f, 0x01, 0x49, 0x02, 0x3f, + 0x01, 0x49, 0x04, 0x3f, 0x01, 0x53, 0x03, 0x66, 0x6f, 0x6f, 0x3f, + 0x01, 0x53, 0x03, 0x62, 0x61, 0x72, 0x24, 0x01, 0x02, 0x00}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result.toString() === '1,2'")); + EXPECT_TRUE(EvaluateScriptForResultBool("result.foo === 'bar'")); + }); + // Sparse array with additional properties. + DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x61, 0xe8, 0x07, 0x3f, 0x01, + 0x53, 0x03, 0x66, 0x6f, 0x6f, 0x3f, 0x01, 0x53, 0x03, + 0x62, 0x61, 0x72, 0x40, 0x01, 0xe8, 0x07, 0x00}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool( + "result.toString() === ','.repeat(999)")); + EXPECT_TRUE(EvaluateScriptForResultBool("result.foo === 'bar'")); + }); + // The distinction between holes and undefined elements must be maintained. + // Note that since the previous output from Chrome fails this test, an + // encoding using the sparse format was constructed instead. + DecodeTest( + {0xff, 0x09, 0x61, 0x02, 0x49, 0x02, 0x5f, 0x40, 0x01, 0x02}, + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE( + EvaluateScriptForResultBool("typeof result[0] === 'undefined'")); + EXPECT_TRUE( + EvaluateScriptForResultBool("typeof result[1] === 'undefined'")); + EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(0)")); + EXPECT_TRUE(EvaluateScriptForResultBool("result.hasOwnProperty(1)")); + }); +} + +TEST_F(ValueSerializerTest, RoundTripArrayWithNonEnumerableElement) { + // Even though this array looks like [1,5,3], the 5 should be missing from the + // perspective of structured clone, which only clones properties that were + // enumerable. + RoundTripTest( + "(() => {" + " var x = [1,2,3];" + " Object.defineProperty(x, '1', {enumerable:false, value:5});" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(3, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty('1')")); + }); +} + +TEST_F(ValueSerializerTest, RoundTripArrayWithTrickyGetters) { + // If an element is deleted before it is serialized, then it's deleted. + RoundTripTest( + "(() => {" + " var x = [{ get a() { delete x[1]; }}, 42];" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE( + EvaluateScriptForResultBool("typeof result[1] === 'undefined'")); + EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(1)")); + }); + // Same for sparse arrays. + RoundTripTest( + "(() => {" + " var x = [{ get a() { delete x[1]; }}, 42];" + " x.length = 1000;" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE( + EvaluateScriptForResultBool("typeof result[1] === 'undefined'")); + EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(1)")); + }); + // If the length is changed, then the resulting array still has the original + // length, but elements that were not yet serialized are gone. + RoundTripTest( + "(() => {" + " var x = [1, { get a() { x.length = 0; }}, 3, 4];" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(4, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === 1")); + EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(2)")); + }); + // Same for sparse arrays. + RoundTripTest( + "(() => {" + " var x = [1, { get a() { x.length = 0; }}, 3, 4];" + " x.length = 1000;" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === 1")); + EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(2)")); + }); + // If a getter makes a property non-enumerable, it should still be enumerated + // as enumeration happens once before getters are invoked. + RoundTripTest( + "(() => {" + " var x = [{ get a() {" + " Object.defineProperty(x, '1', { value: 3, enumerable: false });" + " }}, 2];" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === 3")); + }); + // Same for sparse arrays. + RoundTripTest( + "(() => {" + " var x = [{ get a() {" + " Object.defineProperty(x, '1', { value: 3, enumerable: false });" + " }}, 2];" + " x.length = 1000;" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === 3")); + }); + // Getters on the array itself must also run. + RoundTripTest( + "(() => {" + " var x = [1, 2, 3];" + " Object.defineProperty(x, '1', { enumerable: true, get: () => 4 });" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(3, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === 4")); + }); + // Same for sparse arrays. + RoundTripTest( + "(() => {" + " var x = [1, 2, 3];" + " Object.defineProperty(x, '1', { enumerable: true, get: () => 4 });" + " x.length = 1000;" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === 4")); + }); + // Even with a getter that deletes things, we don't read from the prototype. + RoundTripTest( + "(() => {" + " var x = [{ get a() { delete x[1]; } }, 2];" + " x.__proto__ = Object.create(Array.prototype, { 1: { value: 6 } });" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(2, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("!(1 in result)")); + }); + // Same for sparse arrays. + RoundTripTest( + "(() => {" + " var x = [{ get a() { delete x[1]; } }, 2];" + " x.__proto__ = Object.create(Array.prototype, { 1: { value: 6 } });" + " x.length = 1000;" + " return x;" + "})()", + [this](Local value) { + ASSERT_TRUE(value->IsArray()); + ASSERT_EQ(1000, Array::Cast(*value)->Length()); + EXPECT_TRUE(EvaluateScriptForResultBool("!(1 in result)")); + }); +} + } // namespace } // namespace v8