blob: 75f902f3b26703480d551437517bd4537865a484 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/android/java/gin_java_script_to_java_types_coercion.h"
#include <unistd.h>
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "content/common/android/gin_java_bridge_value.h"
#include "third_party/WebKit/public/platform/WebString.h"
namespace content {
namespace {
const char kJavaLangString[] = "java/lang/String";
const char kUndefined[] = "undefined";
// This is an intermediate solution until we fix http://crbug.com/391492.
jstring ConvertUTF8ToJString(JNIEnv* env, const std::string& string) {
base::string16 utf16(
blink::WebString::fromUTF8(string.c_str(), string.size()));
return env->NewString(utf16.data(), utf16.length());
}
double RoundDoubleTowardsZero(const double& x) {
if (std::isnan(x)) {
return 0.0;
}
return x > 0.0 ? floor(x) : ceil(x);
}
// Rounds to jlong using Java's type conversion rules.
jlong RoundDoubleToLong(const double& x) {
double intermediate = RoundDoubleTowardsZero(x);
// The int64 limits can not be converted exactly to double values, so we
// compare to custom constants. kint64max is 2^63 - 1, but the spacing
// between double values in the the range 2^62 to 2^63 is 2^10. The cast is
// required to silence a spurious gcc warning for integer overflow.
const int64 kLimit = (GG_INT64_C(1) << 63) - static_cast<uint64>(1 << 10);
DCHECK(kLimit > 0);
const double kLargestDoubleLessThanInt64Max = kLimit;
const double kSmallestDoubleGreaterThanInt64Min = -kLimit;
if (intermediate > kLargestDoubleLessThanInt64Max) {
return kint64max;
}
if (intermediate < kSmallestDoubleGreaterThanInt64Min) {
return kint64min;
}
return static_cast<jlong>(intermediate);
}
// Rounds to jint using Java's type conversion rules.
jint RoundDoubleToInt(const double& x) {
double intermediate = RoundDoubleTowardsZero(x);
// The int32 limits cast exactly to double values.
intermediate = std::min(intermediate, static_cast<double>(kint32max));
intermediate = std::max(intermediate, static_cast<double>(kint32min));
return static_cast<jint>(intermediate);
}
jvalue CoerceJavaScriptIntegerToJavaValue(JNIEnv* env,
const base::Value* value,
const JavaType& target_type,
bool coerce_to_string) {
// See http://jdk6.java.net/plugin2/liveconnect/#JS_NUMBER_VALUES.
// For conversion to numeric types, we need to replicate Java's type
// conversion rules. This requires that for integer values, we simply discard
// all but the lowest n buts, where n is the number of bits in the target
// type.
jvalue result;
int int_value;
value->GetAsInteger(&int_value);
switch (target_type.type) {
case JavaType::TypeByte:
result.b = static_cast<jbyte>(int_value);
break;
case JavaType::TypeChar:
result.c = static_cast<jchar>(int_value);
break;
case JavaType::TypeShort:
result.s = static_cast<jshort>(int_value);
break;
case JavaType::TypeInt:
result.i = int_value;
break;
case JavaType::TypeLong:
result.j = int_value;
break;
case JavaType::TypeFloat:
result.f = int_value;
break;
case JavaType::TypeDouble:
result.d = int_value;
break;
case JavaType::TypeObject:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to null. Spec
// requires handling object equivalents of primitive types.
result.l = NULL;
break;
case JavaType::TypeString:
result.l = coerce_to_string
? ConvertUTF8ToJString(env, base::Int64ToString(int_value))
: NULL;
break;
case JavaType::TypeBoolean:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to false. Spec
// requires converting to false for 0 or NaN, true otherwise.
result.z = JNI_FALSE;
break;
case JavaType::TypeArray:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to null. Spec
// requires raising a JavaScript exception.
result.l = NULL;
break;
case JavaType::TypeVoid:
// Conversion to void must never happen.
NOTREACHED();
break;
}
return result;
}
jvalue CoerceJavaScriptDoubleToJavaValue(JNIEnv* env,
double double_value,
const JavaType& target_type,
bool coerce_to_string) {
// See http://jdk6.java.net/plugin2/liveconnect/#JS_NUMBER_VALUES.
// For conversion to numeric types, we need to replicate Java's type
// conversion rules.
jvalue result;
switch (target_type.type) {
case JavaType::TypeByte:
result.b = static_cast<jbyte>(RoundDoubleToInt(double_value));
break;
case JavaType::TypeChar:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert double to 0.
// Spec requires converting doubles similarly to how we convert doubles to
// other numeric types.
result.c = 0;
break;
case JavaType::TypeShort:
result.s = static_cast<jshort>(RoundDoubleToInt(double_value));
break;
case JavaType::TypeInt:
result.i = RoundDoubleToInt(double_value);
break;
case JavaType::TypeLong:
result.j = RoundDoubleToLong(double_value);
break;
case JavaType::TypeFloat:
result.f = static_cast<jfloat>(double_value);
break;
case JavaType::TypeDouble:
result.d = double_value;
break;
case JavaType::TypeObject:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to null. Spec
// requires handling object equivalents of primitive types.
result.l = NULL;
break;
case JavaType::TypeString:
result.l = coerce_to_string
? ConvertUTF8ToJString(
env, base::StringPrintf("%.6lg", double_value))
: NULL;
break;
case JavaType::TypeBoolean:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to false. Spec
// requires converting to false for 0 or NaN, true otherwise.
result.z = JNI_FALSE;
break;
case JavaType::TypeArray:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to null. Spec
// requires raising a JavaScript exception.
result.l = NULL;
break;
case JavaType::TypeVoid:
// Conversion to void must never happen.
NOTREACHED();
break;
}
return result;
}
jvalue CoerceJavaScriptBooleanToJavaValue(JNIEnv* env,
const base::Value* value,
const JavaType& target_type,
bool coerce_to_string) {
// See http://jdk6.java.net/plugin2/liveconnect/#JS_BOOLEAN_VALUES.
bool boolean_value;
value->GetAsBoolean(&boolean_value);
jvalue result;
switch (target_type.type) {
case JavaType::TypeBoolean:
result.z = boolean_value ? JNI_TRUE : JNI_FALSE;
break;
case JavaType::TypeObject:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec
// requires handling java.lang.Boolean and java.lang.Object.
result.l = NULL;
break;
case JavaType::TypeString:
result.l = coerce_to_string ? ConvertUTF8ToJString(
env, boolean_value ? "true" : "false")
: NULL;
break;
case JavaType::TypeByte:
case JavaType::TypeChar:
case JavaType::TypeShort:
case JavaType::TypeInt:
case JavaType::TypeLong:
case JavaType::TypeFloat:
case JavaType::TypeDouble: {
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to 0. Spec
// requires converting to 0 or 1.
jvalue null_value = {0};
result = null_value;
break;
}
case JavaType::TypeArray:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec
// requires raising a JavaScript exception.
result.l = NULL;
break;
case JavaType::TypeVoid:
// Conversion to void must never happen.
NOTREACHED();
break;
}
return result;
}
jvalue CoerceJavaScriptStringToJavaValue(JNIEnv* env,
const base::Value* value,
const JavaType& target_type) {
// See http://jdk6.java.net/plugin2/liveconnect/#JS_STRING_VALUES.
jvalue result;
switch (target_type.type) {
case JavaType::TypeString: {
std::string string_result;
value->GetAsString(&string_result);
result.l = ConvertUTF8ToJString(env, string_result);
break;
}
case JavaType::TypeObject:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec
// requires handling java.lang.Object.
result.l = NULL;
break;
case JavaType::TypeByte:
case JavaType::TypeShort:
case JavaType::TypeInt:
case JavaType::TypeLong:
case JavaType::TypeFloat:
case JavaType::TypeDouble: {
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to 0. Spec
// requires using valueOf() method of corresponding object type.
jvalue null_value = {0};
result = null_value;
break;
}
case JavaType::TypeChar:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to 0. Spec
// requires using java.lang.Short.decode().
result.c = 0;
break;
case JavaType::TypeBoolean:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to false. Spec
// requires converting the empty string to false, otherwise true.
result.z = JNI_FALSE;
break;
case JavaType::TypeArray:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec
// requires raising a JavaScript exception.
result.l = NULL;
break;
case JavaType::TypeVoid:
// Conversion to void must never happen.
NOTREACHED();
break;
}
return result;
}
// Note that this only handles primitive types and strings.
jobject CreateJavaArray(JNIEnv* env, const JavaType& type, jsize length) {
switch (type.type) {
case JavaType::TypeBoolean:
return env->NewBooleanArray(length);
case JavaType::TypeByte:
return env->NewByteArray(length);
case JavaType::TypeChar:
return env->NewCharArray(length);
case JavaType::TypeShort:
return env->NewShortArray(length);
case JavaType::TypeInt:
return env->NewIntArray(length);
case JavaType::TypeLong:
return env->NewLongArray(length);
case JavaType::TypeFloat:
return env->NewFloatArray(length);
case JavaType::TypeDouble:
return env->NewDoubleArray(length);
case JavaType::TypeString: {
base::android::ScopedJavaLocalRef<jclass> clazz(
base::android::GetClass(env, kJavaLangString));
return env->NewObjectArray(length, clazz.obj(), NULL);
}
case JavaType::TypeVoid:
// Conversion to void must never happen.
case JavaType::TypeArray:
case JavaType::TypeObject:
// Not handled.
NOTREACHED();
}
return NULL;
}
// Sets the specified element of the supplied array to the value of the
// supplied jvalue. Requires that the type of the array matches that of the
// jvalue. Handles only primitive types and strings. Note that in the case of a
// string, the array takes a new reference to the string object.
void SetArrayElement(JNIEnv* env,
jobject array,
const JavaType& type,
jsize index,
const jvalue& value) {
switch (type.type) {
case JavaType::TypeBoolean:
env->SetBooleanArrayRegion(static_cast<jbooleanArray>(array), index, 1,
&value.z);
break;
case JavaType::TypeByte:
env->SetByteArrayRegion(static_cast<jbyteArray>(array), index, 1,
&value.b);
break;
case JavaType::TypeChar:
env->SetCharArrayRegion(static_cast<jcharArray>(array), index, 1,
&value.c);
break;
case JavaType::TypeShort:
env->SetShortArrayRegion(static_cast<jshortArray>(array), index, 1,
&value.s);
break;
case JavaType::TypeInt:
env->SetIntArrayRegion(static_cast<jintArray>(array), index, 1,
&value.i);
break;
case JavaType::TypeLong:
env->SetLongArrayRegion(static_cast<jlongArray>(array), index, 1,
&value.j);
break;
case JavaType::TypeFloat:
env->SetFloatArrayRegion(static_cast<jfloatArray>(array), index, 1,
&value.f);
break;
case JavaType::TypeDouble:
env->SetDoubleArrayRegion(static_cast<jdoubleArray>(array), index, 1,
&value.d);
break;
case JavaType::TypeString:
env->SetObjectArrayElement(static_cast<jobjectArray>(array), index,
value.l);
break;
case JavaType::TypeVoid:
// Conversion to void must never happen.
case JavaType::TypeArray:
case JavaType::TypeObject:
// Not handled.
NOTREACHED();
}
base::android::CheckException(env);
}
jvalue CoerceJavaScriptNullOrUndefinedToJavaValue(JNIEnv* env,
const base::Value* value,
const JavaType& target_type,
bool coerce_to_string) {
bool is_undefined = false;
scoped_ptr<const GinJavaBridgeValue> gin_value;
if (GinJavaBridgeValue::ContainsGinJavaBridgeValue(value)) {
gin_value = GinJavaBridgeValue::FromValue(value);
if (gin_value->IsType(GinJavaBridgeValue::TYPE_UNDEFINED)) {
is_undefined = true;
}
}
jvalue result;
switch (target_type.type) {
case JavaType::TypeObject:
result.l = NULL;
break;
case JavaType::TypeString:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert undefined to
// "undefined". Spec requires converting undefined to NULL.
result.l = (coerce_to_string && is_undefined)
? ConvertUTF8ToJString(env, kUndefined)
: NULL;
break;
case JavaType::TypeByte:
case JavaType::TypeChar:
case JavaType::TypeShort:
case JavaType::TypeInt:
case JavaType::TypeLong:
case JavaType::TypeFloat:
case JavaType::TypeDouble: {
jvalue null_value = {0};
result = null_value;
break;
}
case JavaType::TypeBoolean:
result.z = JNI_FALSE;
break;
case JavaType::TypeArray:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec
// requires raising a JavaScript exception.
result.l = NULL;
break;
case JavaType::TypeVoid:
// Conversion to void must never happen.
NOTREACHED();
break;
}
return result;
}
jobject CoerceJavaScriptListToArray(JNIEnv* env,
const base::Value* value,
const JavaType& target_type,
const ObjectRefs& object_refs) {
DCHECK_EQ(JavaType::TypeArray, target_type.type);
const JavaType& target_inner_type = *target_type.inner_type.get();
// LIVECONNECT_COMPLIANCE: Existing behavior is to return null for
// multi-dimensional arrays. Spec requires handling multi-demensional arrays.
if (target_inner_type.type == JavaType::TypeArray) {
return NULL;
}
// LIVECONNECT_COMPLIANCE: Existing behavior is to return null for object
// arrays. Spec requires handling object arrays.
if (target_inner_type.type == JavaType::TypeObject) {
return NULL;
}
const base::ListValue* list_value;
value->GetAsList(&list_value);
// Create the Java array.
jsize length = static_cast<jsize>(list_value->GetSize());
jobject result = CreateJavaArray(env, target_inner_type, length);
if (!result) {
return NULL;
}
scoped_ptr<base::Value> null_value(base::Value::CreateNullValue());
for (jsize i = 0; i < length; ++i) {
const base::Value* value_element = null_value.get();
list_value->Get(i, &value_element);
jvalue element = CoerceJavaScriptValueToJavaValue(
env, value_element, target_inner_type, false, object_refs);
SetArrayElement(env, result, target_inner_type, i, element);
// CoerceJavaScriptValueToJavaValue() creates new local references to
// strings, objects and arrays. Of these, only strings can occur here.
// SetArrayElement() causes the array to take its own reference to the
// string, so we can now release the local reference.
DCHECK_NE(JavaType::TypeObject, target_inner_type.type);
DCHECK_NE(JavaType::TypeArray, target_inner_type.type);
ReleaseJavaValueIfRequired(env, &element, target_inner_type);
}
return result;
}
jobject CoerceJavaScriptDictionaryToArray(JNIEnv* env,
const base::Value* value,
const JavaType& target_type,
const ObjectRefs& object_refs) {
DCHECK_EQ(JavaType::TypeArray, target_type.type);
const JavaType& target_inner_type = *target_type.inner_type.get();
// LIVECONNECT_COMPLIANCE: Existing behavior is to return null for
// multi-dimensional arrays. Spec requires handling multi-demensional arrays.
if (target_inner_type.type == JavaType::TypeArray) {
return NULL;
}
// LIVECONNECT_COMPLIANCE: Existing behavior is to return null for object
// arrays. Spec requires handling object arrays.
if (target_inner_type.type == JavaType::TypeObject) {
return NULL;
}
const base::DictionaryValue* dictionary_value;
value->GetAsDictionary(&dictionary_value);
const base::Value* length_value;
// If the object does not have a length property, return null.
if (!dictionary_value->Get("length", &length_value)) {
return NULL;
}
// If the length property does not have numeric type, or is outside the valid
// range for a Java array length, return null.
jsize length = -1;
if (length_value->IsType(base::Value::TYPE_INTEGER)) {
int int_length;
length_value->GetAsInteger(&int_length);
if (int_length >= 0 && int_length <= kint32max) {
length = static_cast<jsize>(int_length);
}
} else if (length_value->IsType(base::Value::TYPE_DOUBLE)) {
double double_length;
length_value->GetAsDouble(&double_length);
if (double_length >= 0.0 && double_length <= kint32max) {
length = static_cast<jsize>(double_length);
}
}
if (length == -1) {
return NULL;
}
jobject result = CreateJavaArray(env, target_inner_type, length);
if (!result) {
return NULL;
}
scoped_ptr<base::Value> null_value(base::Value::CreateNullValue());
for (jsize i = 0; i < length; ++i) {
const std::string key(base::IntToString(i));
const base::Value* value_element = null_value.get();
if (dictionary_value->HasKey(key)) {
dictionary_value->Get(key, &value_element);
}
jvalue element = CoerceJavaScriptValueToJavaValue(
env, value_element, target_inner_type, false, object_refs);
SetArrayElement(env, result, target_inner_type, i, element);
// CoerceJavaScriptValueToJavaValue() creates new local references to
// strings, objects and arrays. Of these, only strings can occur here.
// SetArrayElement() causes the array to take its own reference to the
// string, so we can now release the local reference.
DCHECK_NE(JavaType::TypeObject, target_inner_type.type);
DCHECK_NE(JavaType::TypeArray, target_inner_type.type);
ReleaseJavaValueIfRequired(env, &element, target_inner_type);
}
return result;
}
jvalue CoerceJavaScriptObjectToJavaValue(JNIEnv* env,
const base::Value* value,
const JavaType& target_type,
bool coerce_to_string,
const ObjectRefs& object_refs) {
// This covers both JavaScript objects (including arrays) and Java objects.
// See http://jdk6.java.net/plugin2/liveconnect/#JS_OTHER_OBJECTS,
// http://jdk6.java.net/plugin2/liveconnect/#JS_ARRAY_VALUES and
// http://jdk6.java.net/plugin2/liveconnect/#JS_JAVA_OBJECTS
jvalue result;
switch (target_type.type) {
case JavaType::TypeObject: {
if (GinJavaBridgeValue::ContainsGinJavaBridgeValue(value)) {
scoped_ptr<const GinJavaBridgeValue> gin_value(
GinJavaBridgeValue::FromValue(value));
DCHECK(gin_value);
DCHECK(gin_value->IsType(GinJavaBridgeValue::TYPE_OBJECT_ID));
base::android::ScopedJavaLocalRef<jobject> obj;
GinJavaBoundObject::ObjectID object_id;
if (gin_value->GetAsObjectID(&object_id)) {
ObjectRefs::const_iterator iter = object_refs.find(object_id);
if (iter != object_refs.end()) {
obj.Reset(iter->second.get(env));
}
}
result.l = obj.Release();
} else {
// LIVECONNECT_COMPLIANCE: Existing behavior is to pass null. Spec
// requires converting if the target type is
// netscape.javascript.JSObject, otherwise raising a JavaScript
// exception.
result.l = NULL;
}
break;
}
case JavaType::TypeString:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to
// "undefined". Spec requires calling toString() on the Java object.
result.l =
coerce_to_string ? ConvertUTF8ToJString(env, kUndefined) : NULL;
break;
case JavaType::TypeByte:
case JavaType::TypeShort:
case JavaType::TypeInt:
case JavaType::TypeLong:
case JavaType::TypeFloat:
case JavaType::TypeDouble:
case JavaType::TypeChar: {
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to 0. Spec
// requires raising a JavaScript exception.
jvalue null_value = {0};
result = null_value;
break;
}
case JavaType::TypeBoolean:
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to false. Spec
// requires raising a JavaScript exception.
result.z = JNI_FALSE;
break;
case JavaType::TypeArray:
if (value->IsType(base::Value::TYPE_DICTIONARY)) {
result.l = CoerceJavaScriptDictionaryToArray(
env, value, target_type, object_refs);
} else if (value->IsType(base::Value::TYPE_LIST)) {
result.l =
CoerceJavaScriptListToArray(env, value, target_type, object_refs);
} else {
result.l = NULL;
}
break;
case JavaType::TypeVoid:
// Conversion to void must never happen.
NOTREACHED();
break;
}
return result;
}
jvalue CoerceGinJavaBridgeValueToJavaValue(JNIEnv* env,
const base::Value* value,
const JavaType& target_type,
bool coerce_to_string,
const ObjectRefs& object_refs) {
DCHECK(GinJavaBridgeValue::ContainsGinJavaBridgeValue(value));
scoped_ptr<const GinJavaBridgeValue> gin_value(
GinJavaBridgeValue::FromValue(value));
switch (gin_value->GetType()) {
case GinJavaBridgeValue::TYPE_UNDEFINED:
return CoerceJavaScriptNullOrUndefinedToJavaValue(
env, value, target_type, coerce_to_string);
case GinJavaBridgeValue::TYPE_NONFINITE: {
float float_value;
gin_value->GetAsNonFinite(&float_value);
return CoerceJavaScriptDoubleToJavaValue(
env, float_value, target_type, coerce_to_string);
}
case GinJavaBridgeValue::TYPE_OBJECT_ID:
return CoerceJavaScriptObjectToJavaValue(
env, value, target_type, coerce_to_string, object_refs);
default:
NOTREACHED();
}
return jvalue();
}
} // namespace
void ReleaseJavaValueIfRequired(JNIEnv* env,
jvalue* value,
const JavaType& type) {
if (type.type == JavaType::TypeString || type.type == JavaType::TypeObject ||
type.type == JavaType::TypeArray) {
env->DeleteLocalRef(value->l);
value->l = NULL;
}
}
jvalue CoerceJavaScriptValueToJavaValue(JNIEnv* env,
const base::Value* value,
const JavaType& target_type,
bool coerce_to_string,
const ObjectRefs& object_refs) {
// Note that in all these conversions, the relevant field of the jvalue must
// always be explicitly set, as jvalue does not initialize its fields.
switch (value->GetType()) {
case base::Value::TYPE_INTEGER:
return CoerceJavaScriptIntegerToJavaValue(
env, value, target_type, coerce_to_string);
case base::Value::TYPE_DOUBLE: {
double double_value;
value->GetAsDouble(&double_value);
return CoerceJavaScriptDoubleToJavaValue(
env, double_value, target_type, coerce_to_string);
}
case base::Value::TYPE_BOOLEAN:
return CoerceJavaScriptBooleanToJavaValue(
env, value, target_type, coerce_to_string);
case base::Value::TYPE_STRING:
return CoerceJavaScriptStringToJavaValue(env, value, target_type);
case base::Value::TYPE_DICTIONARY:
case base::Value::TYPE_LIST:
return CoerceJavaScriptObjectToJavaValue(
env, value, target_type, coerce_to_string, object_refs);
case base::Value::TYPE_NULL:
return CoerceJavaScriptNullOrUndefinedToJavaValue(
env, value, target_type, coerce_to_string);
case base::Value::TYPE_BINARY:
return CoerceGinJavaBridgeValueToJavaValue(
env, value, target_type, coerce_to_string, object_refs);
}
NOTREACHED();
return jvalue();
}
} // namespace content