Revision: 12842
Author: [email protected]
Date: Fri Nov 2 07:46:57 2012
Log: Handle edge cases in basic JSON.stringify.
BUG=
Review URL: https://chromiumcodereview.appspot.com/11315009
http://code.google.com/p/v8/source/detail?r=12842
Modified:
/branches/bleeding_edge/src/json-stringifier.h
/branches/bleeding_edge/src/json.js
/branches/bleeding_edge/test/mjsunit/json2.js
/branches/bleeding_edge/test/mjsunit/regress/regress-json-stringify-gc.js
=======================================
--- /branches/bleeding_edge/src/json-stringifier.h Tue Oct 30 08:29:34 2012
+++ /branches/bleeding_edge/src/json-stringifier.h Fri Nov 2 07:46:57 2012
@@ -46,7 +46,7 @@
static const int kMaxPartLength = 16 * 1024;
static const int kPartLengthGrowthFactor = 2;
- enum Result { UNCHANGED, SUCCESS, BAILOUT, CIRCULAR, STACK_OVERFLOW };
+ enum Result { UNCHANGED, SUCCESS, EXCEPTION, CIRCULAR, STACK_OVERFLOW };
template <bool is_ascii> void Extend();
@@ -79,27 +79,36 @@
Handle<Object> GetProperty(Handle<JSObject> object,
Handle<String> key);
- bool MayHaveToJsonFunction(Handle<JSObject> object);
+ Handle<Object> ApplyToJsonFunction(Handle<Object> object,
+ Handle<Object> key);
- INLINE(Result Serialize(Handle<Object> object)) {
- return Serialize_<false>(object);
+ // Entry point to serialize the object.
+ INLINE(Result SerializeObject(Handle<Object> obj)) {
+ return Serialize_<false>(obj, false,
isolate_->factory()->empty_string());
}
- INLINE(Result SerializeDeferred(Handle<Object> object,
- bool deferred_comma,
- Handle<String> deferred_key)) {
+ // Serialize an array element.
+ // The index may serve as argument for the toJSON function.
+ INLINE(Result SerializeElement(Handle<Object> object, int i)) {
+ return Serialize_<false>(object, false,
Handle<Object>(Smi::FromInt(i)));
+ }
+
+ // Serialize a object property.
+ // The key may or may not be serialized depending on the property.
+ // The key may also serve as argument for the toJSON function.
+ INLINE(Result SerializeProperty(Handle<Object> object,
+ bool deferred_comma,
+ Handle<String> deferred_key)) {
ASSERT(!deferred_key.is_null());
return Serialize_<true>(object, deferred_comma, deferred_key);
}
- template <bool deferred_key>
- Result Serialize_(Handle<Object> object,
- bool comma = false,
- Handle<String> key = Handle<String>::null());
+ template <bool deferred_string_key>
+ Result Serialize_(Handle<Object> object, bool comma, Handle<Object> key);
- void SerializeDeferredKey(bool deferred_comma, Handle<String>
deferred_key) {
+ void SerializeDeferredKey(bool deferred_comma, Handle<Object>
deferred_key) {
if (deferred_comma) Append(',');
- SerializeString(deferred_key);
+ SerializeString(Handle<String>::cast(deferred_key));
Append(':');
}
@@ -110,8 +119,12 @@
return SerializeDouble(object->value());
}
- INLINE(Result SerializeArray(Handle<JSArray> object));
- INLINE(Result SerializeObject(Handle<JSObject> object));
+ Result SerializeJSValue(Handle<JSValue> object);
+
+ INLINE(Result SerializeJSArray(Handle<JSArray> object));
+ INLINE(Result SerializeJSObject(Handle<JSObject> object));
+
+ Result SerializeJSArraySlow(Handle<JSArray> object, int length);
void SerializeString(Handle<String> object);
@@ -207,19 +220,19 @@
MaybeObject* BasicJsonStringifier::Stringify(Handle<Object> object) {
- switch (Serialize(object)) {
+ switch (SerializeObject(object)) {
+ case UNCHANGED:
+ return isolate_->heap()->undefined_value();
case SUCCESS:
ShrinkCurrentPart();
return *isolate_->factory()->NewConsString(accumulator(),
current_part_);
- case UNCHANGED:
- return isolate_->heap()->undefined_value();
case CIRCULAR:
return isolate_->Throw(*isolate_->factory()->NewTypeError(
"circular_structure", HandleVector<Object>(NULL, 0)));
case STACK_OVERFLOW:
return isolate_->StackOverflow();
default:
- return Smi::FromInt(0);
+ return Failure::Exception();
}
}
@@ -261,36 +274,34 @@
}
case CONSTANT_FUNCTION:
return Handle<Object>(lookup.GetConstantFunction());
- case CALLBACKS:
- case HANDLER:
- case INTERCEPTOR:
- return Handle<Object>::null();
- case TRANSITION:
- case NONEXISTENT:
- UNREACHABLE();
- break;
+ default: {
+ PropertyAttributes attr;
+ return Object::GetProperty(object, object, &lookup, key, &attr);
+ }
}
return Handle<Object>::null();
}
-bool BasicJsonStringifier::MayHaveToJsonFunction(Handle<JSObject> object) {
+Handle<Object> BasicJsonStringifier::ApplyToJsonFunction(
+ Handle<Object> object, Handle<Object> key) {
LookupResult lookup(isolate_);
- object->LookupRealNamedProperty(*tojson_symbol_, &lookup);
- if (!lookup.IsProperty()) return false;
- Object* value;
- switch (lookup.type()) {
- case NORMAL:
- value = lookup.holder()->GetNormalizedProperty(&lookup);
- break;
- case FIELD:
- value = lookup.holder()->FastPropertyAt(lookup.GetFieldIndex());
- break;
- default:
- return true;
- }
- ASSERT(!value->IsTheHole());
- return value->IsSpecFunction();
+ JSObject::cast(*object)->LookupRealNamedProperty(*tojson_symbol_,
&lookup);
+ if (!lookup.IsProperty()) return object;
+ PropertyAttributes attr;
+ Handle<Object> fun =
+ Object::GetProperty(object, object, &lookup, tojson_symbol_, &attr);
+ if (!fun->IsJSFunction()) return object;
+
+ // Call toJSON function.
+ if (key->IsSmi()) key = isolate_->factory()->NumberToString(key);
+ Handle<Object> argv[] = { key };
+ bool has_exception = false;
+ HandleScope scope(isolate_);
+ object = Execution::Call(fun, object, 1, argv, &has_exception);
+ // Return empty handle to signal an exception.
+ if (has_exception) return Handle<Object>::null();
+ return scope.CloseAndEscape(object);
}
@@ -319,51 +330,49 @@
}
-template <bool deferred_key>
+template <bool deferred_string_key>
BasicJsonStringifier::Result BasicJsonStringifier::Serialize_(
- Handle<Object> object, bool comma, Handle<String> key) {
+ Handle<Object> object, bool comma, Handle<Object> key) {
if (object->IsJSObject()) {
- // We don't deal with custom toJSON functions.
- if (MayHaveToJsonFunction(Handle<JSObject>::cast(object))) return
BAILOUT;
+ object = ApplyToJsonFunction(object, key);
+ if (object.is_null()) return EXCEPTION;
+ }
- if (object->IsJSFunction()) {
- return UNCHANGED;
- } else if (object->IsJSArray()) {
- if (deferred_key) SerializeDeferredKey(comma, key);
- return SerializeArray(Handle<JSArray>::cast(object));
+ if (object->IsJSObject()) {
+ if (object->IsJSFunction()) return UNCHANGED;
+ if (deferred_string_key) SerializeDeferredKey(comma, key);
+ if (object->IsJSArray()) {
+ return SerializeJSArray(Handle<JSArray>::cast(object));
} else if (object->IsJSValue()) {
- // JSValue with a custom prototype.
- if (object->GetPrototype()->IsJSReceiver()) return BAILOUT;
- // Unpack value wrapper and fall through.
- object = Handle<Object>(JSValue::cast(*object)->value());
+ return SerializeJSValue(Handle<JSValue>::cast(object));
} else {
- if (deferred_key) SerializeDeferredKey(comma, key);
- return SerializeObject(Handle<JSObject>::cast(object));
+ return SerializeJSObject(Handle<JSObject>::cast(object));
}
}
+ // Handle non-JSObject.
if (object->IsString()) {
- if (deferred_key) SerializeDeferredKey(comma, key);
+ if (deferred_string_key) SerializeDeferredKey(comma, key);
SerializeString(Handle<String>::cast(object));
return SUCCESS;
} else if (object->IsSmi()) {
- if (deferred_key) SerializeDeferredKey(comma, key);
+ if (deferred_string_key) SerializeDeferredKey(comma, key);
return SerializeSmi(Smi::cast(*object));
} else if (object->IsHeapNumber()) {
- if (deferred_key) SerializeDeferredKey(comma, key);
+ if (deferred_string_key) SerializeDeferredKey(comma, key);
return SerializeHeapNumber(Handle<HeapNumber>::cast(object));
} else if (object->IsOddball()) {
switch (Oddball::cast(*object)->kind()) {
case Oddball::kFalse:
- if (deferred_key) SerializeDeferredKey(comma, key);
+ if (deferred_string_key) SerializeDeferredKey(comma, key);
Append("false");
return SUCCESS;
case Oddball::kTrue:
- if (deferred_key) SerializeDeferredKey(comma, key);
+ if (deferred_string_key) SerializeDeferredKey(comma, key);
Append("true");
return SUCCESS;
case Oddball::kNull:
- if (deferred_key) SerializeDeferredKey(comma, key);
+ if (deferred_string_key) SerializeDeferredKey(comma, key);
Append("null");
return SUCCESS;
}
@@ -371,6 +380,29 @@
return UNCHANGED;
}
+
+
+BasicJsonStringifier::Result BasicJsonStringifier::SerializeJSValue(
+ Handle<JSValue> object) {
+ bool has_exception = false;
+ String* class_name = object->class_name();
+ if (class_name == isolate_->heap()->String_symbol()) {
+ Handle<Object> value = Execution::ToString(object, &has_exception);
+ if (has_exception) return EXCEPTION;
+ SerializeString(Handle<String>::cast(value));
+ } else if (class_name == isolate_->heap()->Number_symbol()) {
+ Handle<Object> value = Execution::ToNumber(object, &has_exception);
+ if (has_exception) return EXCEPTION;
+ if (value->IsSmi()) return SerializeSmi(Smi::cast(*value));
+ SerializeHeapNumber(Handle<HeapNumber>::cast(value));
+ } else {
+ ASSERT(class_name == isolate_->heap()->Boolean_symbol());
+ Object* value = JSValue::cast(*object)->value();
+ ASSERT(value->IsBoolean());
+ Append(value->IsTrue() ? "true" : "false");
+ }
+ return SUCCESS;
+}
BasicJsonStringifier::Result BasicJsonStringifier::SerializeSmi(Smi*
object) {
@@ -396,7 +428,7 @@
}
-BasicJsonStringifier::Result BasicJsonStringifier::SerializeArray(
+BasicJsonStringifier::Result BasicJsonStringifier::SerializeJSArray(
Handle<JSArray> object) {
HandleScope handle_scope(isolate_);
Result stack_push = StackPush(object);
@@ -405,8 +437,7 @@
Append('[');
switch (object->GetElementsKind()) {
case FAST_SMI_ELEMENTS: {
- Handle<FixedArray> elements = Handle<FixedArray>(
- FixedArray::cast(object->elements()));
+ Handle<FixedArray> elements(FixedArray::cast(object->elements()));
for (int i = 0; i < length; i++) {
if (i > 0) Append(',');
SerializeSmi(Smi::cast(elements->get(i)));
@@ -414,7 +445,7 @@
break;
}
case FAST_DOUBLE_ELEMENTS: {
- Handle<FixedDoubleArray> elements = Handle<FixedDoubleArray>(
+ Handle<FixedDoubleArray> elements(
FixedDoubleArray::cast(object->elements()));
for (int i = 0; i < length; i++) {
if (i > 0) Append(',');
@@ -423,11 +454,10 @@
break;
}
case FAST_ELEMENTS: {
- Handle<FixedArray> elements = Handle<FixedArray>(
- FixedArray::cast(object->elements()));
+ Handle<FixedArray> elements(FixedArray::cast(object->elements()));
for (int i = 0; i < length; i++) {
if (i > 0) Append(',');
- Result result = Serialize(Handle<Object>(elements->get(i)));
+ Result result = SerializeElement(Handle<Object>(elements->get(i)),
i);
if (result == SUCCESS) continue;
if (result == UNCHANGED) {
Append("null");
@@ -437,8 +467,14 @@
}
break;
}
- default:
- return BAILOUT;
+ // TODO(yangguo): The FAST_HOLEY_* cases could be handled in a faster
way.
+ // They resemble the non-holey cases except that a prototype chain
lookup
+ // is necessary for holes.
+ default: {
+ Result result = SerializeJSArraySlow(object, length);
+ if (result != SUCCESS) return result;
+ break;
+ }
}
Append(']');
StackPop();
@@ -447,16 +483,40 @@
}
-BasicJsonStringifier::Result BasicJsonStringifier::SerializeObject(
+BasicJsonStringifier::Result BasicJsonStringifier::SerializeJSArraySlow(
+ Handle<JSArray> object, int length) {
+ for (int i = 0; i < length; i++) {
+ if (i > 0) Append(',');
+ Handle<Object> element = Object::GetElement(object, i);
+ if (element->IsUndefined()) {
+ Append("null");
+ } else {
+ Result result = SerializeElement(element, i);
+ if (result == SUCCESS) continue;
+ if (result == UNCHANGED) {
+ Append("null");
+ } else {
+ return result;
+ }
+ }
+ }
+ return SUCCESS;
+}
+
+
+BasicJsonStringifier::Result BasicJsonStringifier::SerializeJSObject(
Handle<JSObject> object) {
HandleScope handle_scope(isolate_);
Result stack_push = StackPush(object);
if (stack_push != SUCCESS) return stack_push;
- if (object->IsJSGlobalProxy()) return BAILOUT;
- bool threw = false;
+ if (object->IsJSGlobalProxy()) {
+ object = Handle<JSObject>(JSObject::cast(object->GetPrototype()));
+ ASSERT(object->IsGlobalObject());
+ }
+ bool has_exception = false;
Handle<FixedArray> contents =
- GetKeysInFixedArrayFor(object, LOCAL_ONLY, &threw);
- if (threw) return BAILOUT;
+ GetKeysInFixedArrayFor(object, LOCAL_ONLY, &has_exception);
+ if (has_exception) return EXCEPTION;
Append('{');
bool comma = false;
for (int i = 0; i < contents->length(); i++) {
@@ -478,10 +538,10 @@
property = GetProperty(object, key_handle);
}
}
- if (property.is_null()) return BAILOUT;
- Result result = SerializeDeferred(property, comma, key_handle);
+ if (property.is_null()) return EXCEPTION;
+ Result result = SerializeProperty(property, comma, key_handle);
if (!comma && result == SUCCESS) comma = true;
- if (result >= BAILOUT) return result;
+ if (result >= EXCEPTION) return result;
}
Append('}');
StackPop();
=======================================
--- /branches/bleeding_edge/src/json.js Mon Oct 22 07:22:58 2012
+++ /branches/bleeding_edge/src/json.js Fri Nov 2 07:46:57 2012
@@ -176,145 +176,11 @@
// Undefined or a callable object.
return void 0;
}
-
-
-function BasicSerializeArray(value, stack, builder) {
- var len = value.length;
- if (len == 0) {
- builder.push("[]");
- return;
- }
- if (!%PushIfAbsent(stack, value)) {
- throw MakeTypeError('circular_structure', $Array());
- }
- builder.push("[");
- var val = value[0];
- if (IS_STRING(val)) {
- // First entry is a string. Remaining entries are likely to be strings
too.
- var array_string = %QuoteJSONStringArray(value);
- if (!IS_UNDEFINED(array_string)) {
- // array_string also includes bracket characters so we are done.
- builder[builder.length - 1] = array_string;
- stack.pop();
- return;
- } else {
- builder.push(%QuoteJSONString(val));
- for (var i = 1; i < len; i++) {
- val = value[i];
- if (IS_STRING(val)) {
- builder.push(%QuoteJSONStringComma(val));
- } else {
- builder.push(",");
- var before = builder.length;
- BasicJSONSerialize(i, val, stack, builder);
- if (before == builder.length) builder[before - 1] = ",null";
- }
- }
- }
- } else if (IS_NUMBER(val)) {
- // First entry is a number. Remaining entries are likely to be numbers
too.
- builder.push(JSON_NUMBER_TO_STRING(val));
- for (var i = 1; i < len; i++) {
- builder.push(",");
- val = value[i];
- if (IS_NUMBER(val)) {
- builder.push(JSON_NUMBER_TO_STRING(val));
- } else {
- var before = builder.length;
- BasicJSONSerialize(i, val, stack, builder);
- if (before == builder.length) builder[before - 1] = ",null";
- }
- }
- } else {
- var before = builder.length;
- BasicJSONSerialize(0, val, stack, builder);
- if (before == builder.length) builder.push("null");
- for (var i = 1; i < len; i++) {
- builder.push(",");
- before = builder.length;
- BasicJSONSerialize(i, value[i], stack, builder);
- if (before == builder.length) builder[before - 1] = ",null";
- }
- }
- stack.pop();
- builder.push("]");
-}
-
-
-function BasicSerializeObject(value, stack, builder) {
- if (!%PushIfAbsent(stack, value)) {
- throw MakeTypeError('circular_structure', $Array());
- }
- builder.push("{");
- var first = true;
- var keys = %ObjectKeys(value);
- var len = keys.length;
- for (var i = 0; i < len; i++) {
- var p = keys[i];
- if (!first) {
- builder.push(%QuoteJSONStringComma(p));
- } else {
- builder.push(%QuoteJSONString(p));
- }
- builder.push(":");
- var before = builder.length;
- BasicJSONSerialize(p, value[p], stack, builder);
- if (before == builder.length) {
- builder.pop();
- builder.pop();
- } else {
- first = false;
- }
- }
- stack.pop();
- builder.push("}");
-}
-
-
-function BasicJSONSerialize(key, value, stack, builder) {
- if (IS_SPEC_OBJECT(value)) {
- var toJSON = value.toJSON;
- if (IS_SPEC_FUNCTION(toJSON)) {
- value = %_CallFunction(value, ToString(key), toJSON);
- }
- }
- if (IS_STRING(value)) {
- builder.push(value !== "" ? %QuoteJSONString(value) : '""');
- } else if (IS_NUMBER(value)) {
- builder.push(JSON_NUMBER_TO_STRING(value));
- } else if (IS_BOOLEAN(value)) {
- builder.push(value ? "true" : "false");
- } else if (IS_NULL(value)) {
- builder.push("null");
- } else if (IS_SPEC_OBJECT(value) && !(typeof value == "function")) {
- // Value is a non-callable object.
- // Unwrap value if necessary
- if (IS_NUMBER_WRAPPER(value)) {
- value = ToNumber(value);
- builder.push(JSON_NUMBER_TO_STRING(value));
- } else if (IS_STRING_WRAPPER(value)) {
- builder.push(%QuoteJSONString(ToString(value)));
- } else if (IS_BOOLEAN_WRAPPER(value)) {
- builder.push(%_ValueOf(value) ? "true" : "false");
- } else if (IS_ARRAY(value)) {
- BasicSerializeArray(value, stack, builder);
- } else {
- BasicSerializeObject(value, stack, builder);
- }
- }
-}
function JSONStringify(value, replacer, space) {
if (%_ArgumentsLength() == 1) {
- var result = %BasicJSONStringify(value);
- if (result != 0) return result;
- var builder = new InternalArray();
- BasicJSONSerialize('', value, new InternalArray(), builder);
- if (builder.length == 0) return;
- result = %_FastAsciiArrayJoin(builder, "");
- if (!IS_UNDEFINED(result)) return result;
- return %StringBuilderConcat(builder, builder.length, "");
+ return %BasicJSONStringify(value);
}
if (IS_OBJECT(space)) {
// Unwrap 'space' if it is wrapped
@@ -339,6 +205,7 @@
}
return JSONSerialize('', {'': value}, replacer, new InternalArray(), "",
gap);
}
+
function SetUpJSON() {
%CheckIsBootstrapping();
=======================================
--- /branches/bleeding_edge/test/mjsunit/json2.js Tue Oct 30 08:29:34 2012
+++ /branches/bleeding_edge/test/mjsunit/json2.js Fri Nov 2 07:46:57 2012
@@ -27,6 +27,89 @@
// Flags: --allow-natives-syntax
+// Test JSON.stringify on the global object.
+var a = 12345;
+assertTrue(JSON.stringify(this).indexOf('"a":12345') > 0);
+
+// Test JSON.stringify of array in dictionary mode.
+var array_1 = [];
+var array_2 = [];
+array_1[100000] = 1;
+array_2[100000] = function() { return 1; };
+var nulls = "";
+for (var i = 0; i < 100000; i++) {
+ nulls += 'null,';
+}
+expected_1 = '[' + nulls + '1]';
+expected_2 = '[' + nulls + 'null]';
+assertEquals(expected_1, JSON.stringify(array_1));
+assertEquals(expected_2, JSON.stringify(array_2));
+
+// Test JSValue with custom prototype.
+var num_wrapper = Object(42);
+num_wrapper.__proto__ = { __proto__: null,
+ toString: function() { return true; } };
+assertEquals('1', JSON.stringify(num_wrapper));
+
+var str_wrapper = Object('2');
+str_wrapper.__proto__ = { __proto__: null,
+ toString: function() { return true; } };
+assertEquals('"true"', JSON.stringify(str_wrapper));
+
+var bool_wrapper = Object(false);
+bool_wrapper.__proto__ = { __proto__: null,
+ toString: function() { return true; } };
+// Note that toString function is not evaluated here!
+assertEquals('false', JSON.stringify(bool_wrapper));
+
+// Test getters.
+var counter = 0;
+var getter_obj = { get getter() {
+ counter++;
+ return 123;
+ } };
+assertEquals('{"getter":123}', JSON.stringify(getter_obj));
+assertEquals(1, counter);
+
+// Test toJSON function.
+var tojson_obj = { toJSON: function() {
+ counter++;
+ return [1, 2];
+ },
+ a: 1};
+assertEquals('[1,2]', JSON.stringify(tojson_obj));
+assertEquals(2, counter);
+
+// Test that we don't recursively look for the toJSON function.
+var tojson_proto_obj = { a: 'fail' };
+tojson_proto_obj.__proto__ = { toJSON: function() {
+ counter++;
+ return tojson_obj;
+ } };
+assertEquals('{"a":1}', JSON.stringify(tojson_proto_obj));
+
+// Test toJSON produced by a getter.
+var tojson_via_getter = { get toJSON() {
+ return function(x) {
+ counter++;
+ return 321;
+ };
+ },
+ a: 1 };
+assertEquals('321', JSON.stringify(tojson_via_getter));
+
+// Test toJSON with key.
+tojson_obj = { toJSON: function(key) { return key + key; } };
+var tojson_with_key_1 = { a: tojson_obj, b: tojson_obj };
+assertEquals('{"a":"aa","b":"bb"}', JSON.stringify(tojson_with_key_1));
+var tojson_with_key_2 = [ tojson_obj, tojson_obj ];
+assertEquals('["00","11"]', JSON.stringify(tojson_with_key_2));
+
+// Test toJSON with exception.
+var tojson_ex = { toJSON: function(key) { throw "123" } };
+assertThrows(function() { JSON.stringify(tojson_ex); });
+
+// Test holes in arrays.
var fast_smi = [1, 2, 3, 4];
fast_smi.__proto__ = [7, 7, 7, 7];
delete fast_smi[2];
=======================================
---
/branches/bleeding_edge/test/mjsunit/regress/regress-json-stringify-gc.js
Mon Oct 22 07:22:58 2012
+++
/branches/bleeding_edge/test/mjsunit/regress/regress-json-stringify-gc.js
Fri Nov 2 07:46:57 2012
@@ -37,5 +37,5 @@
// screw up reading from the correct location.
json1 = JSON.stringify(a);
json2 = JSON.stringify(a);
-assertEquals(json1, json2);
+assertEquals(json1, json2, "GC caused JSON.stringify to fail.");
--
v8-dev mailing list
[email protected]
http://groups.google.com/group/v8-dev