Blink-compatible serialization of Map and Set objects.

BUG=chromium:148757

Review-Url: https://codereview.chromium.org/2269923004
Cr-Commit-Position: refs/heads/master@{#38871}
This commit is contained in:
jbroman 2016-08-24 08:58:34 -07:00 committed by Commit bot
parent eba4ae2357
commit 78131aa1d5
3 changed files with 351 additions and 0 deletions

View File

@ -82,6 +82,14 @@ enum class SerializationTag : uint8_t {
// Regular expression, UTF-8 encoding. byteLength:uint32_t, raw data,
// flags:uint32_t.
kRegExp = 'R',
// Beginning of a JS map.
kBeginJSMap = ';',
// End of a JS map. length:uint32_t.
kEndJSMap = ':',
// Beginning of a JS set.
kBeginJSSet = '\'',
// End of a JS set. length:uint32_t.
kEndJSSet = ',',
};
ValueSerializer::ValueSerializer(Isolate* isolate)
@ -289,6 +297,10 @@ Maybe<bool> ValueSerializer::WriteJSReceiver(Handle<JSReceiver> receiver) {
case JS_REGEXP_TYPE:
WriteJSRegExp(JSRegExp::cast(*receiver));
return Just(true);
case JS_MAP_TYPE:
return WriteJSMap(Handle<JSMap>::cast(receiver));
case JS_SET_TYPE:
return WriteJSSet(Handle<JSSet>::cast(receiver));
default:
UNIMPLEMENTED();
break;
@ -417,6 +429,67 @@ void ValueSerializer::WriteJSRegExp(JSRegExp* regexp) {
WriteVarint(static_cast<uint32_t>(regexp->GetFlags()));
}
Maybe<bool> ValueSerializer::WriteJSMap(Handle<JSMap> map) {
// First copy the key-value pairs, since getters could mutate them.
Handle<OrderedHashMap> table(OrderedHashMap::cast(map->table()));
int length = table->NumberOfElements() * 2;
Handle<FixedArray> entries = isolate_->factory()->NewFixedArray(length);
{
DisallowHeapAllocation no_gc;
Oddball* the_hole = isolate_->heap()->the_hole_value();
int capacity = table->UsedCapacity();
int result_index = 0;
for (int i = 0; i < capacity; i++) {
Object* key = table->KeyAt(i);
if (key == the_hole) continue;
entries->set(result_index++, key);
entries->set(result_index++, table->ValueAt(i));
}
DCHECK_EQ(result_index, length);
}
// Then write it out.
WriteTag(SerializationTag::kBeginJSMap);
for (int i = 0; i < length; i++) {
if (!WriteObject(handle(entries->get(i), isolate_)).FromMaybe(false)) {
return Nothing<bool>();
}
}
WriteTag(SerializationTag::kEndJSMap);
WriteVarint<uint32_t>(length);
return Just(true);
}
Maybe<bool> ValueSerializer::WriteJSSet(Handle<JSSet> set) {
// First copy the element pointers, since getters could mutate them.
Handle<OrderedHashSet> table(OrderedHashSet::cast(set->table()));
int length = table->NumberOfElements();
Handle<FixedArray> entries = isolate_->factory()->NewFixedArray(length);
{
DisallowHeapAllocation no_gc;
Oddball* the_hole = isolate_->heap()->the_hole_value();
int capacity = table->UsedCapacity();
int result_index = 0;
for (int i = 0; i < capacity; i++) {
Object* key = table->KeyAt(i);
if (key == the_hole) continue;
entries->set(result_index++, key);
}
DCHECK_EQ(result_index, length);
}
// Then write it out.
WriteTag(SerializationTag::kBeginJSSet);
for (int i = 0; i < length; i++) {
if (!WriteObject(handle(entries->get(i), isolate_)).FromMaybe(false)) {
return Nothing<bool>();
}
}
WriteTag(SerializationTag::kEndJSSet);
WriteVarint<uint32_t>(length);
return Just(true);
}
Maybe<uint32_t> ValueSerializer::WriteJSObjectProperties(
Handle<JSObject> object, Handle<FixedArray> keys) {
uint32_t properties_written = 0;
@ -606,6 +679,10 @@ MaybeHandle<Object> ValueDeserializer::ReadObject() {
return ReadJSValue(tag);
case SerializationTag::kRegExp:
return ReadJSRegExp();
case SerializationTag::kBeginJSMap:
return ReadJSMap();
case SerializationTag::kBeginJSSet:
return ReadJSSet();
default:
return MaybeHandle<Object>();
}
@ -801,6 +878,79 @@ MaybeHandle<JSRegExp> ValueDeserializer::ReadJSRegExp() {
return regexp;
}
MaybeHandle<JSMap> ValueDeserializer::ReadJSMap() {
// If we are at the end of the stack, abort. This function may recurse.
if (StackLimitCheck(isolate_).HasOverflowed()) return MaybeHandle<JSMap>();
HandleScope scope(isolate_);
uint32_t id = next_id_++;
Handle<JSMap> map = isolate_->factory()->NewJSMap();
AddObjectWithID(id, map);
Handle<JSFunction> map_set = isolate_->map_set();
uint32_t length = 0;
while (true) {
SerializationTag tag;
if (!PeekTag().To(&tag)) return MaybeHandle<JSMap>();
if (tag == SerializationTag::kEndJSMap) {
ConsumeTag(SerializationTag::kEndJSMap);
break;
}
Handle<Object> argv[2];
if (!ReadObject().ToHandle(&argv[0]) || !ReadObject().ToHandle(&argv[1]) ||
Execution::Call(isolate_, map_set, map, arraysize(argv), argv)
.is_null()) {
return MaybeHandle<JSMap>();
}
length += 2;
}
uint32_t expected_length;
if (!ReadVarint<uint32_t>().To(&expected_length) ||
length != expected_length) {
return MaybeHandle<JSMap>();
}
DCHECK(HasObjectWithID(id));
return scope.CloseAndEscape(map);
}
MaybeHandle<JSSet> ValueDeserializer::ReadJSSet() {
// If we are at the end of the stack, abort. This function may recurse.
if (StackLimitCheck(isolate_).HasOverflowed()) return MaybeHandle<JSSet>();
HandleScope scope(isolate_);
uint32_t id = next_id_++;
Handle<JSSet> set = isolate_->factory()->NewJSSet();
AddObjectWithID(id, set);
Handle<JSFunction> set_add = isolate_->set_add();
uint32_t length = 0;
while (true) {
SerializationTag tag;
if (!PeekTag().To(&tag)) return MaybeHandle<JSSet>();
if (tag == SerializationTag::kEndJSSet) {
ConsumeTag(SerializationTag::kEndJSSet);
break;
}
Handle<Object> argv[1];
if (!ReadObject().ToHandle(&argv[0]) ||
Execution::Call(isolate_, set_add, set, arraysize(argv), argv)
.is_null()) {
return MaybeHandle<JSSet>();
}
length++;
}
uint32_t expected_length;
if (!ReadVarint<uint32_t>().To(&expected_length) ||
length != expected_length) {
return MaybeHandle<JSSet>();
}
DCHECK(HasObjectWithID(id));
return scope.CloseAndEscape(set);
}
Maybe<uint32_t> ValueDeserializer::ReadJSObjectProperties(
Handle<JSObject> object, SerializationTag end_tag) {
for (uint32_t num_properties = 0;; num_properties++) {

View File

@ -21,7 +21,9 @@ namespace internal {
class HeapNumber;
class Isolate;
class JSDate;
class JSMap;
class JSRegExp;
class JSSet;
class JSValue;
class Object;
class Oddball;
@ -79,6 +81,8 @@ class ValueSerializer {
void WriteJSDate(JSDate* date);
Maybe<bool> WriteJSValue(Handle<JSValue> value) WARN_UNUSED_RESULT;
void WriteJSRegExp(JSRegExp* regexp);
Maybe<bool> WriteJSMap(Handle<JSMap> map) WARN_UNUSED_RESULT;
Maybe<bool> WriteJSSet(Handle<JSSet> map) WARN_UNUSED_RESULT;
/*
* Reads the specified keys from the object and writes key-value pairs to the
@ -152,6 +156,8 @@ class ValueDeserializer {
MaybeHandle<JSDate> ReadJSDate() WARN_UNUSED_RESULT;
MaybeHandle<JSValue> ReadJSValue(SerializationTag tag) WARN_UNUSED_RESULT;
MaybeHandle<JSRegExp> ReadJSRegExp() WARN_UNUSED_RESULT;
MaybeHandle<JSMap> ReadJSMap() WARN_UNUSED_RESULT;
MaybeHandle<JSSet> ReadJSSet() WARN_UNUSED_RESULT;
/*
* Reads key-value pairs into the object until the specified end tag is

View File

@ -1322,5 +1322,200 @@ TEST_F(ValueSerializerTest, DecodeRegExp) {
});
}
TEST_F(ValueSerializerTest, RoundTripMap) {
RoundTripTest(
"(() => { var m = new Map(); m.set(42, 'foo'); return m; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsMap());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Object.getPrototypeOf(result) === Map.prototype"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.size === 1"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.get(42) === 'foo'"));
});
RoundTripTest("(() => { var m = new Map(); m.set(m, m); return m; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsMap());
EXPECT_TRUE(EvaluateScriptForResultBool("result.size === 1"));
EXPECT_TRUE(EvaluateScriptForResultBool(
"result.get(result) === result"));
});
// Iteration order must be preserved.
RoundTripTest(
"(() => {"
" var m = new Map();"
" m.set(1, 0); m.set('a', 0); m.set(3, 0); m.set(2, 0);"
" return m;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsMap());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Array.from(result.keys()).toString() === '1,a,3,2'"));
});
}
TEST_F(ValueSerializerTest, DecodeMap) {
DecodeTest(
{0xff, 0x09, 0x3f, 0x00, 0x3b, 0x3f, 0x01, 0x49, 0x54, 0x3f, 0x01, 0x53,
0x03, 0x66, 0x6f, 0x6f, 0x3a, 0x02},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsMap());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Object.getPrototypeOf(result) === Map.prototype"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.size === 1"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.get(42) === 'foo'"));
});
DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x3b, 0x3f, 0x01, 0x5e, 0x00, 0x3f, 0x01,
0x5e, 0x00, 0x3a, 0x02, 0x00},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsMap());
EXPECT_TRUE(EvaluateScriptForResultBool("result.size === 1"));
EXPECT_TRUE(EvaluateScriptForResultBool(
"result.get(result) === result"));
});
// Iteration order must be preserved.
DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x3b, 0x3f, 0x01, 0x49, 0x02, 0x3f,
0x01, 0x49, 0x00, 0x3f, 0x01, 0x53, 0x01, 0x61, 0x3f, 0x01,
0x49, 0x00, 0x3f, 0x01, 0x49, 0x06, 0x3f, 0x01, 0x49, 0x00,
0x3f, 0x01, 0x49, 0x04, 0x3f, 0x01, 0x49, 0x00, 0x3a, 0x08},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsMap());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Array.from(result.keys()).toString() === '1,a,3,2'"));
});
}
TEST_F(ValueSerializerTest, RoundTripMapWithTrickyGetters) {
// Even if an entry is removed or reassigned, the original key/value pair is
// used.
RoundTripTest(
"(() => {"
" var m = new Map();"
" m.set(0, { get a() {"
" m.delete(1); m.set(2, 'baz'); m.set(3, 'quux');"
" }});"
" m.set(1, 'foo');"
" m.set(2, 'bar');"
" return m;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsMap());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Array.from(result.keys()).toString() === '0,1,2'"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.get(1) === 'foo'"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.get(2) === 'bar'"));
});
// However, deeper modifications of objects yet to be serialized still apply.
RoundTripTest(
"(() => {"
" var m = new Map();"
" var key = { get a() { value.foo = 'bar'; } };"
" var value = { get a() { key.baz = 'quux'; } };"
" m.set(key, value);"
" return m;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsMap());
EXPECT_TRUE(EvaluateScriptForResultBool(
"!('baz' in Array.from(result.keys())[0])"));
EXPECT_TRUE(EvaluateScriptForResultBool(
"Array.from(result.values())[0].foo === 'bar'"));
});
}
TEST_F(ValueSerializerTest, RoundTripSet) {
RoundTripTest(
"(() => { var s = new Set(); s.add(42); s.add('foo'); return s; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsSet());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Object.getPrototypeOf(result) === Set.prototype"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.size === 2"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.has(42)"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.has('foo')"));
});
RoundTripTest(
"(() => { var s = new Set(); s.add(s); return s; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsSet());
EXPECT_TRUE(EvaluateScriptForResultBool("result.size === 1"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.has(result)"));
});
// Iteration order must be preserved.
RoundTripTest(
"(() => {"
" var s = new Set();"
" s.add(1); s.add('a'); s.add(3); s.add(2);"
" return s;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsSet());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Array.from(result.keys()).toString() === '1,a,3,2'"));
});
}
TEST_F(ValueSerializerTest, DecodeSet) {
DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x27, 0x3f, 0x01, 0x49, 0x54, 0x3f, 0x01,
0x53, 0x03, 0x66, 0x6f, 0x6f, 0x2c, 0x02},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsSet());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Object.getPrototypeOf(result) === Set.prototype"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.size === 2"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.has(42)"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.has('foo')"));
});
DecodeTest(
{0xff, 0x09, 0x3f, 0x00, 0x27, 0x3f, 0x01, 0x5e, 0x00, 0x2c, 0x01, 0x00},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsSet());
EXPECT_TRUE(EvaluateScriptForResultBool("result.size === 1"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.has(result)"));
});
// Iteration order must be preserved.
DecodeTest(
{0xff, 0x09, 0x3f, 0x00, 0x27, 0x3f, 0x01, 0x49, 0x02, 0x3f, 0x01, 0x53,
0x01, 0x61, 0x3f, 0x01, 0x49, 0x06, 0x3f, 0x01, 0x49, 0x04, 0x2c, 0x04},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsSet());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Array.from(result.keys()).toString() === '1,a,3,2'"));
});
}
TEST_F(ValueSerializerTest, RoundTripSetWithTrickyGetters) {
// Even if an element is added or removed during serialization, the original
// set of elements is used.
RoundTripTest(
"(() => {"
" var s = new Set();"
" s.add({ get a() { s.delete(1); s.add(2); } });"
" s.add(1);"
" return s;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsSet());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Array.from(result.keys()).toString() === '[object Object],1'"));
});
// However, deeper modifications of objects yet to be serialized still apply.
RoundTripTest(
"(() => {"
" var s = new Set();"
" var first = { get a() { second.foo = 'bar'; } };"
" var second = { get a() { first.baz = 'quux'; } };"
" s.add(first);"
" s.add(second);"
" return s;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsSet());
EXPECT_TRUE(EvaluateScriptForResultBool(
"!('baz' in Array.from(result.keys())[0])"));
EXPECT_TRUE(EvaluateScriptForResultBool(
"Array.from(result.keys())[1].foo === 'bar'"));
});
}
} // namespace
} // namespace v8