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:
parent
eba4ae2357
commit
78131aa1d5
@ -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++) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user