blob: f79ea8a9107f44cd2d1da2e4a275ba82ba7cac67 [file] [log] [blame]
// Copyright 2016 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 "extensions/renderer/argument_spec.h"
#include "base/memory/ptr_util.h"
#include "base/values.h"
#include "content/public/child/v8_value_converter.h"
#include "gin/converter.h"
#include "gin/dictionary.h"
namespace extensions {
namespace {
template <class T>
std::unique_ptr<base::Value> GetFundamentalConvertedValueHelper(
v8::Local<v8::Value> arg,
v8::Local<v8::Context> context,
const base::Optional<int>& minimum) {
T val;
if (!gin::Converter<T>::FromV8(context->GetIsolate(), arg, &val))
return nullptr;
if (minimum && val < minimum.value())
return nullptr;
return base::MakeUnique<base::FundamentalValue>(val);
}
} // namespace
ArgumentSpec::ArgumentSpec(const base::Value& value)
: type_(ArgumentType::INTEGER), optional_(false) {
const base::DictionaryValue* dict = nullptr;
CHECK(value.GetAsDictionary(&dict));
dict->GetBoolean("optional", &optional_);
dict->GetString("name", &name_);
InitializeType(dict);
}
void ArgumentSpec::InitializeType(const base::DictionaryValue* dict) {
std::string ref_string;
if (dict->GetString("$ref", &ref_string)) {
ref_ = std::move(ref_string);
type_ = ArgumentType::REF;
return;
}
std::string type_string;
CHECK(dict->GetString("type", &type_string));
if (type_string == "integer")
type_ = ArgumentType::INTEGER;
else if (type_string == "number")
type_ = ArgumentType::DOUBLE;
else if (type_string == "object")
type_ = ArgumentType::OBJECT;
else if (type_string == "array")
type_ = ArgumentType::LIST;
else if (type_string == "boolean")
type_ = ArgumentType::BOOLEAN;
else if (type_string == "string")
type_ = ArgumentType::STRING;
else if (type_string == "any")
type_ = ArgumentType::ANY;
else if (type_string == "function")
type_ = ArgumentType::FUNCTION;
else
NOTREACHED();
int min = 0;
if (dict->GetInteger("minimum", &min))
minimum_ = min;
const base::DictionaryValue* properties_value = nullptr;
if (type_ == ArgumentType::OBJECT &&
dict->GetDictionary("properties", &properties_value)) {
for (base::DictionaryValue::Iterator iter(*properties_value);
!iter.IsAtEnd(); iter.Advance()) {
properties_[iter.key()] = base::MakeUnique<ArgumentSpec>(iter.value());
}
} else if (type_ == ArgumentType::LIST) {
const base::DictionaryValue* item_value = nullptr;
CHECK(dict->GetDictionary("items", &item_value));
list_element_type_ = base::MakeUnique<ArgumentSpec>(*item_value);
} else if (type_ == ArgumentType::STRING) {
// Technically, there's no reason enums couldn't be other objects (e.g.
// numbers), but right now they seem to be exclusively strings. We could
// always update this if need be.
const base::ListValue* enums = nullptr;
if (dict->GetList("enum", &enums)) {
size_t size = enums->GetSize();
CHECK_GT(size, 0u);
for (size_t i = 0; i < size; ++i) {
std::string enum_value;
// Enum entries come in two versions: a list of possible strings, and
// a dictionary with a field 'name'.
if (!enums->GetString(i, &enum_value)) {
const base::DictionaryValue* enum_value_dictionary = nullptr;
CHECK(enums->GetDictionary(i, &enum_value_dictionary));
CHECK(enum_value_dictionary->GetString("name", &enum_value));
}
enum_values_.insert(std::move(enum_value));
}
}
}
}
ArgumentSpec::~ArgumentSpec() {}
std::unique_ptr<base::Value> ArgumentSpec::ConvertArgument(
v8::Local<v8::Context> context,
v8::Local<v8::Value> value,
const RefMap& refs,
std::string* error) const {
// TODO(devlin): Support functions?
DCHECK_NE(type_, ArgumentType::FUNCTION);
if (type_ == ArgumentType::REF) {
DCHECK(ref_);
auto iter = refs.find(ref_.value());
DCHECK(iter != refs.end()) << ref_.value();
return iter->second->ConvertArgument(context, value, refs, error);
}
if (IsFundamentalType())
return ConvertArgumentToFundamental(context, value, error);
if (type_ == ArgumentType::OBJECT) {
// TODO(devlin): Currently, this would accept an array (if that array had
// all the requisite properties). Is that the right thing to do?
if (!value->IsObject()) {
*error = "Wrong type";
return nullptr;
}
v8::Local<v8::Object> object = value.As<v8::Object>();
return ConvertArgumentToObject(context, object, refs, error);
}
if (type_ == ArgumentType::LIST) {
if (!value->IsArray()) {
*error = "Wrong type";
return nullptr;
}
v8::Local<v8::Array> array = value.As<v8::Array>();
return ConvertArgumentToArray(context, array, refs, error);
}
if (type_ == ArgumentType::ANY)
return ConvertArgumentToAny(context, value, error);
NOTREACHED();
return nullptr;
}
bool ArgumentSpec::IsFundamentalType() const {
return type_ == ArgumentType::INTEGER || type_ == ArgumentType::DOUBLE ||
type_ == ArgumentType::BOOLEAN || type_ == ArgumentType::STRING;
}
std::unique_ptr<base::Value> ArgumentSpec::ConvertArgumentToFundamental(
v8::Local<v8::Context> context,
v8::Local<v8::Value> value,
std::string* error) const {
DCHECK(IsFundamentalType());
switch (type_) {
case ArgumentType::INTEGER:
return GetFundamentalConvertedValueHelper<int32_t>(value, context,
minimum_);
case ArgumentType::DOUBLE:
return GetFundamentalConvertedValueHelper<double>(value, context,
minimum_);
case ArgumentType::STRING: {
std::string s;
// TODO(devlin): If base::StringValue ever takes a std::string&&, we could
// use std::move to construct.
if (!gin::Converter<std::string>::FromV8(context->GetIsolate(),
value, &s) ||
(!enum_values_.empty() && enum_values_.count(s) == 0)) {
return nullptr;
}
return base::MakeUnique<base::StringValue>(s);
}
case ArgumentType::BOOLEAN: {
bool b = false;
if (value->IsBoolean() &&
gin::Converter<bool>::FromV8(context->GetIsolate(), value, &b)) {
return base::MakeUnique<base::FundamentalValue>(b);
}
return nullptr;
}
default:
NOTREACHED();
}
return nullptr;
}
std::unique_ptr<base::Value> ArgumentSpec::ConvertArgumentToObject(
v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const RefMap& refs,
std::string* error) const {
DCHECK_EQ(ArgumentType::OBJECT, type_);
auto result = base::MakeUnique<base::DictionaryValue>();
gin::Dictionary dictionary(context->GetIsolate(), object);
for (const auto& kv : properties_) {
v8::Local<v8::Value> subvalue;
// See comment in ConvertArgumentToArray() about passing in custom crazy
// values here.
// TODO(devlin): gin::Dictionary::Get() uses Isolate::GetCurrentContext() -
// is that always right here, or should we use the v8::Object APIs and
// pass in |context|?
// TODO(devlin): Hyper-optimization - Dictionary::Get() also creates a new
// v8::String for each call. Hypothetically, we could cache these, or at
// least use an internalized string.
if (!dictionary.Get(kv.first, &subvalue))
return nullptr;
if (subvalue.IsEmpty() || subvalue->IsNull() || subvalue->IsUndefined()) {
if (!kv.second->optional_) {
*error = "Missing key: " + kv.first;
return nullptr;
}
continue;
}
std::unique_ptr<base::Value> property =
kv.second->ConvertArgument(context, subvalue, refs, error);
if (!property)
return nullptr;
result->Set(kv.first, std::move(property));
}
return std::move(result);
}
std::unique_ptr<base::Value> ArgumentSpec::ConvertArgumentToArray(
v8::Local<v8::Context> context,
v8::Local<v8::Array> value,
const RefMap& refs,
std::string* error) const {
DCHECK_EQ(ArgumentType::LIST, type_);
auto result = base::MakeUnique<base::ListValue>();
uint32_t length = value->Length();
for (uint32_t i = 0; i < length; ++i) {
v8::MaybeLocal<v8::Value> maybe_subvalue = value->Get(context, i);
v8::Local<v8::Value> subvalue;
// Note: This can fail in the case of a developer passing in the following:
// var a = [];
// Object.defineProperty(a, 0, { get: () => { throw new Error('foo'); } });
// Currently, this will cause the developer-specified error ('foo') to be
// thrown.
// TODO(devlin): This is probably fine, but it's worth contemplating
// catching the error and throwing our own.
if (!maybe_subvalue.ToLocal(&subvalue))
return nullptr;
std::unique_ptr<base::Value> item =
list_element_type_->ConvertArgument(context, subvalue, refs, error);
if (!item)
return nullptr;
result->Append(std::move(item));
}
return std::move(result);
}
std::unique_ptr<base::Value> ArgumentSpec::ConvertArgumentToAny(
v8::Local<v8::Context> context,
v8::Local<v8::Value> value,
std::string* error) const {
DCHECK_EQ(ArgumentType::ANY, type_);
std::unique_ptr<content::V8ValueConverter> converter(
content::V8ValueConverter::create());
std::unique_ptr<base::Value> converted(
converter->FromV8Value(value, context));
if (!converted)
*error = "Could not convert to 'any'.";
return converted;
}
} // namespace extensions