| // 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/bindings/argument_spec.h" |
| |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| #include "content/public/child/v8_value_converter.h" |
| #include "extensions/renderer/bindings/api_invocation_errors.h" |
| #include "extensions/renderer/bindings/api_type_reference_map.h" |
| #include "gin/converter.h" |
| #include "gin/dictionary.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Returns a type string for the given |value|. |
| const char* GetV8ValueTypeString(v8::Local<v8::Value> value) { |
| DCHECK(!value.IsEmpty()); |
| |
| if (value->IsNull()) |
| return api_errors::kTypeNull; |
| if (value->IsUndefined()) |
| return api_errors::kTypeUndefined; |
| if (value->IsInt32()) |
| return api_errors::kTypeInteger; |
| if (value->IsNumber()) |
| return api_errors::kTypeDouble; |
| if (value->IsBoolean()) |
| return api_errors::kTypeBoolean; |
| if (value->IsString()) |
| return api_errors::kTypeString; |
| |
| // Note: check IsArray(), IsFunction(), and IsArrayBuffer[View]() before |
| // IsObject() since arrays, functions, and array buffers are objects. |
| if (value->IsArray()) |
| return api_errors::kTypeList; |
| if (value->IsFunction()) |
| return api_errors::kTypeFunction; |
| if (value->IsArrayBuffer() || value->IsArrayBufferView()) |
| return api_errors::kTypeBinary; |
| if (value->IsObject()) |
| return api_errors::kTypeObject; |
| |
| // TODO(devlin): The list above isn't exhaustive (it's missing at least |
| // Symbol and Uint32). We may want to include those, since saying |
| // "expected int, found other" isn't super helpful. On the other hand, authors |
| // should be able to see what they passed. |
| return "other"; |
| } |
| |
| // Returns true if |value| is within the bounds specified by |minimum| and |
| // |maximum|, populating |error| otherwise. |
| template <class T> |
| bool CheckFundamentalBounds(T value, |
| const base::Optional<int>& minimum, |
| const base::Optional<int>& maximum, |
| std::string* error) { |
| if (minimum && value < *minimum) { |
| *error = api_errors::NumberTooSmall(*minimum); |
| return false; |
| } |
| if (maximum && value > *maximum) { |
| *error = api_errors::NumberTooLarge(*maximum); |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| ArgumentSpec::ArgumentSpec(const base::Value& value) { |
| const base::DictionaryValue* dict = nullptr; |
| CHECK(value.GetAsDictionary(&dict)); |
| dict->GetBoolean("optional", &optional_); |
| dict->GetString("name", &name_); |
| |
| InitializeType(dict); |
| } |
| |
| ArgumentSpec::ArgumentSpec(ArgumentType type) : type_(type) {} |
| |
| 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; |
| } |
| |
| { |
| const base::ListValue* choices = nullptr; |
| if (dict->GetList("choices", &choices)) { |
| DCHECK(!choices->empty()); |
| type_ = ArgumentType::CHOICES; |
| choices_.reserve(choices->GetSize()); |
| for (const auto& choice : *choices) |
| choices_.push_back(base::MakeUnique<ArgumentSpec>(choice)); |
| 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 == "binary") |
| type_ = ArgumentType::BINARY; |
| 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; |
| |
| int max = 0; |
| if (dict->GetInteger("maximum", &max)) |
| maximum_ = max; |
| |
| int min_length = 0; |
| if (dict->GetInteger("minLength", &min_length) || |
| dict->GetInteger("minItems", &min_length)) { |
| DCHECK_GE(min_length, 0); |
| min_length_ = min_length; |
| } |
| |
| int max_length = 0; |
| if (dict->GetInteger("maxLength", &max_length) || |
| dict->GetInteger("maxItems", &max_length)) { |
| DCHECK_GE(max_length, 0); |
| max_length_ = max_length; |
| } |
| |
| if (type_ == ArgumentType::OBJECT) { |
| const base::DictionaryValue* properties_value = nullptr; |
| if (dict->GetDictionary("properties", &properties_value)) { |
| for (base::DictionaryValue::Iterator iter(*properties_value); |
| !iter.IsAtEnd(); iter.Advance()) { |
| properties_[iter.key()] = base::MakeUnique<ArgumentSpec>(iter.value()); |
| } |
| } |
| const base::DictionaryValue* additional_properties_value = nullptr; |
| if (dict->GetDictionary("additionalProperties", |
| &additional_properties_value)) { |
| additional_properties_ = |
| base::MakeUnique<ArgumentSpec>(*additional_properties_value); |
| // Additional properties are always optional. |
| additional_properties_->optional_ = true; |
| } |
| } 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)); |
| } |
| } |
| } |
| |
| // Check if we should preserve null in objects. Right now, this is only used |
| // on arguments of type object and any (in fact, it's only used in the storage |
| // API), but it could potentially make sense for lists or functions as well. |
| if (type_ == ArgumentType::OBJECT || type_ == ArgumentType::ANY) |
| dict->GetBoolean("preserveNull", &preserve_null_); |
| |
| if (type_ == ArgumentType::OBJECT || type_ == ArgumentType::BINARY) { |
| std::string instance_of; |
| if (dict->GetString("isInstanceOf", &instance_of)) |
| instance_of_ = instance_of; |
| } |
| } |
| |
| ArgumentSpec::~ArgumentSpec() {} |
| |
| bool ArgumentSpec::IsCorrectType(v8::Local<v8::Value> value, |
| const APITypeReferenceMap& refs, |
| std::string* error) const { |
| bool is_valid_type = false; |
| |
| switch (type_) { |
| case ArgumentType::INTEGER: |
| is_valid_type = value->IsInt32(); |
| break; |
| case ArgumentType::DOUBLE: |
| is_valid_type = value->IsNumber(); |
| break; |
| case ArgumentType::BOOLEAN: |
| is_valid_type = value->IsBoolean(); |
| break; |
| case ArgumentType::STRING: |
| is_valid_type = value->IsString(); |
| break; |
| case ArgumentType::OBJECT: |
| // Don't allow functions or arrays (even though they are technically |
| // objects). This is to make it easier to match otherwise-ambiguous |
| // signatures. For instance, if an API method has an optional object |
| // parameter and then an optional callback, we wouldn't necessarily be |
| // able to match the arguments if we allowed functions as objects. |
| // TODO(devlin): What about other subclasses of Object, like Map and Set? |
| is_valid_type = |
| value->IsObject() && !value->IsFunction() && !value->IsArray(); |
| break; |
| case ArgumentType::LIST: |
| is_valid_type = value->IsArray(); |
| break; |
| case ArgumentType::BINARY: |
| is_valid_type = value->IsArrayBuffer() || value->IsArrayBufferView(); |
| break; |
| case ArgumentType::FUNCTION: |
| is_valid_type = value->IsFunction(); |
| break; |
| case ArgumentType::ANY: |
| is_valid_type = true; |
| break; |
| case ArgumentType::REF: { |
| DCHECK(ref_); |
| const ArgumentSpec* reference = refs.GetSpec(ref_.value()); |
| DCHECK(reference) << ref_.value(); |
| is_valid_type = reference->IsCorrectType(value, refs, error); |
| break; |
| } |
| case ArgumentType::CHOICES: |
| for (const auto& choice : choices_) { |
| if (choice->IsCorrectType(value, refs, error)) { |
| is_valid_type = true; |
| break; |
| } |
| } |
| break; |
| } |
| |
| if (!is_valid_type) |
| *error = GetInvalidTypeError(value); |
| return is_valid_type; |
| } |
| |
| bool ArgumentSpec::ParseArgument(v8::Local<v8::Context> context, |
| v8::Local<v8::Value> value, |
| const APITypeReferenceMap& refs, |
| std::unique_ptr<base::Value>* out_value, |
| std::string* error) const { |
| // Note: for top-level arguments (i.e., those passed directly to the function, |
| // as opposed to a property on an object, or the item of an array), we will |
| // have already checked the type. Doing so again should be nearly free, but |
| // if we do find this to be an issue, we could avoid the second call. |
| if (!IsCorrectType(value, refs, error)) |
| return false; |
| |
| switch (type_) { |
| case ArgumentType::INTEGER: |
| case ArgumentType::DOUBLE: |
| case ArgumentType::BOOLEAN: |
| case ArgumentType::STRING: |
| return ParseArgumentToFundamental(context, value, out_value, error); |
| case ArgumentType::OBJECT: |
| return ParseArgumentToObject(context, value.As<v8::Object>(), refs, |
| out_value, error); |
| case ArgumentType::LIST: |
| return ParseArgumentToArray(context, value.As<v8::Array>(), refs, |
| out_value, error); |
| case ArgumentType::BINARY: |
| return ParseArgumentToAny(context, value, out_value, error); |
| case ArgumentType::FUNCTION: |
| if (out_value) { |
| // Certain APIs (contextMenus) have functions as parameters other than |
| // the callback (contextMenus uses it for an onclick listener). Our |
| // generated types have adapted to consider functions "objects" and |
| // serialize them as dictionaries. |
| // TODO(devlin): It'd be awfully nice to get rid of this eccentricity. |
| *out_value = base::MakeUnique<base::DictionaryValue>(); |
| } |
| return true; |
| case ArgumentType::REF: { |
| DCHECK(ref_); |
| const ArgumentSpec* reference = refs.GetSpec(ref_.value()); |
| DCHECK(reference) << ref_.value(); |
| return reference->ParseArgument(context, value, refs, out_value, error); |
| } |
| case ArgumentType::CHOICES: { |
| for (const auto& choice : choices_) { |
| if (choice->ParseArgument(context, value, refs, out_value, error)) |
| return true; |
| } |
| *error = api_errors::InvalidChoice(); |
| return false; |
| } |
| case ArgumentType::ANY: |
| return ParseArgumentToAny(context, value, out_value, error); |
| } |
| |
| NOTREACHED(); |
| return false; |
| } |
| |
| const std::string& ArgumentSpec::GetTypeName() const { |
| if (!type_name_.empty()) |
| return type_name_; |
| |
| switch (type_) { |
| case ArgumentType::INTEGER: |
| type_name_ = api_errors::kTypeInteger; |
| break; |
| case ArgumentType::DOUBLE: |
| type_name_ = api_errors::kTypeDouble; |
| break; |
| case ArgumentType::BOOLEAN: |
| type_name_ = api_errors::kTypeBoolean; |
| break; |
| case ArgumentType::STRING: |
| type_name_ = api_errors::kTypeString; |
| break; |
| case ArgumentType::OBJECT: |
| type_name_ = instance_of_ ? *instance_of_ : api_errors::kTypeObject; |
| break; |
| case ArgumentType::LIST: |
| type_name_ = api_errors::kTypeList; |
| break; |
| case ArgumentType::BINARY: |
| type_name_ = api_errors::kTypeBinary; |
| break; |
| case ArgumentType::FUNCTION: |
| type_name_ = api_errors::kTypeFunction; |
| break; |
| case ArgumentType::REF: |
| type_name_ = ref_->c_str(); |
| break; |
| case ArgumentType::CHOICES: { |
| std::vector<base::StringPiece> choices_strings; |
| choices_strings.reserve(choices_.size()); |
| for (const auto& choice : choices_) |
| choices_strings.push_back(choice->GetTypeName()); |
| type_name_ = base::StringPrintf( |
| "[%s]", base::JoinString(choices_strings, "|").c_str()); |
| break; |
| } |
| case ArgumentType::ANY: |
| type_name_ = api_errors::kTypeAny; |
| break; |
| } |
| DCHECK(!type_name_.empty()); |
| return type_name_; |
| } |
| |
| bool ArgumentSpec::ParseArgumentToFundamental( |
| v8::Local<v8::Context> context, |
| v8::Local<v8::Value> value, |
| std::unique_ptr<base::Value>* out_value, |
| std::string* error) const { |
| switch (type_) { |
| case ArgumentType::INTEGER: { |
| DCHECK(value->IsInt32()); |
| int int_val = value.As<v8::Int32>()->Value(); |
| if (!CheckFundamentalBounds(int_val, minimum_, maximum_, error)) |
| return false; |
| if (out_value) |
| *out_value = base::MakeUnique<base::Value>(int_val); |
| return true; |
| } |
| case ArgumentType::DOUBLE: { |
| DCHECK(value->IsNumber()); |
| double double_val = value.As<v8::Number>()->Value(); |
| if (!CheckFundamentalBounds(double_val, minimum_, maximum_, error)) |
| return false; |
| if (out_value) |
| *out_value = base::MakeUnique<base::Value>(double_val); |
| return true; |
| } |
| case ArgumentType::STRING: { |
| DCHECK(value->IsString()); |
| |
| v8::Local<v8::String> v8_string = value.As<v8::String>(); |
| size_t length = static_cast<size_t>(v8_string->Length()); |
| if (min_length_ && length < *min_length_) { |
| *error = api_errors::TooFewStringChars(*min_length_, length); |
| return false; |
| } |
| |
| if (max_length_ && length > *max_length_) { |
| *error = api_errors::TooManyStringChars(*max_length_, length); |
| return false; |
| } |
| |
| // If we don't need to match enum values and don't need to convert, we're |
| // done... |
| if (!out_value && enum_values_.empty()) |
| return true; |
| // ...Otherwise, we need to convert to a std::string. |
| std::string s; |
| // We already checked that this is a string, so this should never fail. |
| CHECK(gin::Converter<std::string>::FromV8(context->GetIsolate(), value, |
| &s)); |
| if (!enum_values_.empty() && enum_values_.count(s) == 0) { |
| *error = api_errors::InvalidEnumValue(enum_values_); |
| return false; |
| } |
| if (out_value) { |
| // TODO(devlin): If base::Value ever takes a std::string&&, we |
| // could use std::move to construct. |
| *out_value = base::MakeUnique<base::Value>(s); |
| } |
| return true; |
| } |
| case ArgumentType::BOOLEAN: { |
| DCHECK(value->IsBoolean()); |
| if (out_value) { |
| *out_value = |
| base::MakeUnique<base::Value>(value.As<v8::Boolean>()->Value()); |
| } |
| return true; |
| } |
| default: |
| NOTREACHED(); |
| } |
| return false; |
| } |
| |
| bool ArgumentSpec::ParseArgumentToObject( |
| v8::Local<v8::Context> context, |
| v8::Local<v8::Object> object, |
| const APITypeReferenceMap& refs, |
| std::unique_ptr<base::Value>* out_value, |
| std::string* error) const { |
| DCHECK_EQ(ArgumentType::OBJECT, type_); |
| std::unique_ptr<base::DictionaryValue> result; |
| // Only construct the result if we have an |out_value| to populate. |
| if (out_value) |
| result = base::MakeUnique<base::DictionaryValue>(); |
| |
| v8::Local<v8::Array> own_property_names; |
| if (!object->GetOwnPropertyNames(context).ToLocal(&own_property_names)) { |
| *error = api_errors::ScriptThrewError(); |
| return false; |
| } |
| |
| // Track all properties we see from |properties_| to check if any are missing. |
| // Use ArgumentSpec* instead of std::string for comparison + copy efficiency. |
| std::set<const ArgumentSpec*> seen_properties; |
| uint32_t length = own_property_names->Length(); |
| std::string property_error; |
| for (uint32_t i = 0; i < length; ++i) { |
| v8::Local<v8::Value> key; |
| if (!own_property_names->Get(context, i).ToLocal(&key)) { |
| *error = api_errors::ScriptThrewError(); |
| return false; |
| } |
| // In JS, all keys are strings or numbers (or symbols, but those are |
| // excluded by GetOwnPropertyNames()). If you try to set anything else |
| // (e.g. an object), it is converted to a string. |
| DCHECK(key->IsString() || key->IsNumber()); |
| v8::String::Utf8Value utf8_key(key); |
| |
| ArgumentSpec* property_spec = nullptr; |
| auto iter = properties_.find(*utf8_key); |
| bool allow_unserializable = false; |
| if (iter != properties_.end()) { |
| property_spec = iter->second.get(); |
| seen_properties.insert(property_spec); |
| } else if (additional_properties_) { |
| property_spec = additional_properties_.get(); |
| // additionalProperties: {type: any} is often used to allow anything |
| // through, including things that would normally break serialization like |
| // functions, or even NaN. If the additional properties are of |
| // ArgumentType::ANY, allow anything, even if it doesn't serialize. |
| allow_unserializable = property_spec->type_ == ArgumentType::ANY; |
| } else { |
| *error = api_errors::UnexpectedProperty(*utf8_key); |
| return false; |
| } |
| |
| v8::Local<v8::Value> prop_value; |
| // Fun: It's possible that a previous getter has removed the property from |
| // the object. This isn't that big of a deal, since it would only manifest |
| // in the case of some reasonably-crazy script objects, and it's probably |
| // not worth optimizing for the uncommon case to the detriment of the |
| // common (and either should be totally safe). We can always add a |
| // HasOwnProperty() check here in the future, if we desire. |
| // See also comment in ParseArgumentToArray() about passing in custom |
| // crazy values here. |
| if (!object->Get(context, key).ToLocal(&prop_value)) { |
| *error = api_errors::ScriptThrewError(); |
| return false; |
| } |
| |
| // Note: We don't serialize undefined, and only serialize null if it's part |
| // of the spec. |
| // TODO(devlin): This matches current behavior, but it is correct? And |
| // we treat undefined and null the same? |
| if (prop_value->IsUndefined() || prop_value->IsNull()) { |
| if (!property_spec->optional_) { |
| *error = api_errors::MissingRequiredProperty(*utf8_key); |
| return false; |
| } |
| if (preserve_null_ && prop_value->IsNull() && result) { |
| result->SetWithoutPathExpansion(*utf8_key, |
| base::MakeUnique<base::Value>()); |
| } |
| continue; |
| } |
| |
| std::unique_ptr<base::Value> property; |
| if (!property_spec->ParseArgument(context, prop_value, refs, |
| result ? &property : nullptr, |
| &property_error)) { |
| if (allow_unserializable) |
| continue; |
| *error = api_errors::PropertyError(*utf8_key, property_error); |
| return false; |
| } |
| if (result) |
| result->SetWithoutPathExpansion(*utf8_key, std::move(property)); |
| } |
| |
| for (const auto& pair : properties_) { |
| const ArgumentSpec* spec = pair.second.get(); |
| if (!spec->optional_ && seen_properties.count(spec) == 0) { |
| *error = api_errors::MissingRequiredProperty(pair.first.c_str()); |
| return false; |
| } |
| } |
| |
| if (instance_of_) { |
| // Check for the instance somewhere in the object's prototype chain. |
| // NOTE: This only checks that something in the prototype chain was |
| // constructed with the same name as the desired instance, but doesn't |
| // validate that it's the same constructor as the expected one. For |
| // instance, if we expect isInstanceOf == 'Date', script could pass in |
| // (function() { |
| // function Date() {} |
| // return new Date(); |
| // })() |
| // Since the object contains 'Date' in its prototype chain, this check |
| // succeeds, even though the object is not of built-in type Date. |
| // Since this isn't (or at least shouldn't be) a security check, this is |
| // okay. |
| bool found = false; |
| v8::Local<v8::Value> next_check = object; |
| do { |
| v8::Local<v8::Object> current = next_check.As<v8::Object>(); |
| v8::String::Utf8Value constructor(current->GetConstructorName()); |
| if (*instance_of_ == |
| base::StringPiece(*constructor, constructor.length())) { |
| found = true; |
| break; |
| } |
| next_check = current->GetPrototype(); |
| } while (next_check->IsObject()); |
| |
| if (!found) { |
| *error = api_errors::NotAnInstance(instance_of_->c_str()); |
| return false; |
| } |
| } |
| |
| if (out_value) |
| *out_value = std::move(result); |
| return true; |
| } |
| |
| bool ArgumentSpec::ParseArgumentToArray(v8::Local<v8::Context> context, |
| v8::Local<v8::Array> value, |
| const APITypeReferenceMap& refs, |
| std::unique_ptr<base::Value>* out_value, |
| std::string* error) const { |
| DCHECK_EQ(ArgumentType::LIST, type_); |
| |
| uint32_t length = value->Length(); |
| if (min_length_ && length < *min_length_) { |
| *error = api_errors::TooFewArrayItems(*min_length_, length); |
| return false; |
| } |
| |
| if (max_length_ && length > *max_length_) { |
| *error = api_errors::TooManyArrayItems(*max_length_, length); |
| return false; |
| } |
| |
| std::unique_ptr<base::ListValue> result; |
| // Only construct the result if we have an |out_value| to populate. |
| if (out_value) |
| result = base::MakeUnique<base::ListValue>(); |
| |
| std::string item_error; |
| 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 false; |
| std::unique_ptr<base::Value> item; |
| if (!list_element_type_->ParseArgument( |
| context, subvalue, refs, result ? &item : nullptr, &item_error)) { |
| *error = api_errors::IndexError(i, item_error); |
| return false; |
| } |
| if (result) |
| result->Append(std::move(item)); |
| } |
| if (out_value) |
| *out_value = std::move(result); |
| return true; |
| } |
| |
| bool ArgumentSpec::ParseArgumentToAny(v8::Local<v8::Context> context, |
| v8::Local<v8::Value> value, |
| std::unique_ptr<base::Value>* out_value, |
| std::string* error) const { |
| DCHECK(type_ == ArgumentType::ANY || type_ == ArgumentType::BINARY); |
| if (out_value) { |
| std::unique_ptr<content::V8ValueConverter> converter = |
| content::V8ValueConverter::Create(); |
| converter->SetStripNullFromObjects(!preserve_null_); |
| std::unique_ptr<base::Value> converted = |
| converter->FromV8Value(value, context); |
| if (!converted) { |
| *error = api_errors::UnserializableValue(); |
| return false; |
| } |
| if (type_ == ArgumentType::BINARY) |
| DCHECK_EQ(base::Value::Type::BINARY, converted->GetType()); |
| *out_value = std::move(converted); |
| } |
| return true; |
| } |
| |
| std::string ArgumentSpec::GetInvalidTypeError( |
| v8::Local<v8::Value> value) const { |
| return api_errors::InvalidType(GetTypeName().c_str(), |
| GetV8ValueTypeString(value)); |
| } |
| |
| } // namespace extensions |