Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(16)

Side by Side Diff: extensions/renderer/argument_spec.cc

Issue 2947463002: [Extensions Bindings] Add a bindings/ subdirectory under renderer (Closed)
Patch Set: . Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « extensions/renderer/argument_spec.h ('k') | extensions/renderer/argument_spec_builder.h » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "extensions/renderer/argument_spec.h"
6
7 #include "base/memory/ptr_util.h"
8 #include "base/strings/string_piece.h"
9 #include "base/strings/string_util.h"
10 #include "base/strings/stringprintf.h"
11 #include "base/values.h"
12 #include "content/public/child/v8_value_converter.h"
13 #include "extensions/renderer/api_invocation_errors.h"
14 #include "extensions/renderer/api_type_reference_map.h"
15 #include "gin/converter.h"
16 #include "gin/dictionary.h"
17
18 namespace extensions {
19
20 namespace {
21
22 // Returns a type string for the given |value|.
23 const char* GetV8ValueTypeString(v8::Local<v8::Value> value) {
24 DCHECK(!value.IsEmpty());
25
26 if (value->IsNull())
27 return api_errors::kTypeNull;
28 if (value->IsUndefined())
29 return api_errors::kTypeUndefined;
30 if (value->IsInt32())
31 return api_errors::kTypeInteger;
32 if (value->IsNumber())
33 return api_errors::kTypeDouble;
34 if (value->IsBoolean())
35 return api_errors::kTypeBoolean;
36 if (value->IsString())
37 return api_errors::kTypeString;
38
39 // Note: check IsArray(), IsFunction(), and IsArrayBuffer[View]() before
40 // IsObject() since arrays, functions, and array buffers are objects.
41 if (value->IsArray())
42 return api_errors::kTypeList;
43 if (value->IsFunction())
44 return api_errors::kTypeFunction;
45 if (value->IsArrayBuffer() || value->IsArrayBufferView())
46 return api_errors::kTypeBinary;
47 if (value->IsObject())
48 return api_errors::kTypeObject;
49
50 // TODO(devlin): The list above isn't exhaustive (it's missing at least
51 // Symbol and Uint32). We may want to include those, since saying
52 // "expected int, found other" isn't super helpful. On the other hand, authors
53 // should be able to see what they passed.
54 return "other";
55 }
56
57 // Returns true if |value| is within the bounds specified by |minimum| and
58 // |maximum|, populating |error| otherwise.
59 template <class T>
60 bool CheckFundamentalBounds(T value,
61 const base::Optional<int>& minimum,
62 const base::Optional<int>& maximum,
63 std::string* error) {
64 if (minimum && value < *minimum) {
65 *error = api_errors::NumberTooSmall(*minimum);
66 return false;
67 }
68 if (maximum && value > *maximum) {
69 *error = api_errors::NumberTooLarge(*maximum);
70 return false;
71 }
72 return true;
73 }
74
75 } // namespace
76
77 ArgumentSpec::ArgumentSpec(const base::Value& value) {
78 const base::DictionaryValue* dict = nullptr;
79 CHECK(value.GetAsDictionary(&dict));
80 dict->GetBoolean("optional", &optional_);
81 dict->GetString("name", &name_);
82
83 InitializeType(dict);
84 }
85
86 ArgumentSpec::ArgumentSpec(ArgumentType type) : type_(type) {}
87
88 void ArgumentSpec::InitializeType(const base::DictionaryValue* dict) {
89 std::string ref_string;
90 if (dict->GetString("$ref", &ref_string)) {
91 ref_ = std::move(ref_string);
92 type_ = ArgumentType::REF;
93 return;
94 }
95
96 {
97 const base::ListValue* choices = nullptr;
98 if (dict->GetList("choices", &choices)) {
99 DCHECK(!choices->empty());
100 type_ = ArgumentType::CHOICES;
101 choices_.reserve(choices->GetSize());
102 for (const auto& choice : *choices)
103 choices_.push_back(base::MakeUnique<ArgumentSpec>(choice));
104 return;
105 }
106 }
107
108 std::string type_string;
109 CHECK(dict->GetString("type", &type_string));
110 if (type_string == "integer")
111 type_ = ArgumentType::INTEGER;
112 else if (type_string == "number")
113 type_ = ArgumentType::DOUBLE;
114 else if (type_string == "object")
115 type_ = ArgumentType::OBJECT;
116 else if (type_string == "array")
117 type_ = ArgumentType::LIST;
118 else if (type_string == "boolean")
119 type_ = ArgumentType::BOOLEAN;
120 else if (type_string == "string")
121 type_ = ArgumentType::STRING;
122 else if (type_string == "binary")
123 type_ = ArgumentType::BINARY;
124 else if (type_string == "any")
125 type_ = ArgumentType::ANY;
126 else if (type_string == "function")
127 type_ = ArgumentType::FUNCTION;
128 else
129 NOTREACHED();
130
131 int min = 0;
132 if (dict->GetInteger("minimum", &min))
133 minimum_ = min;
134
135 int max = 0;
136 if (dict->GetInteger("maximum", &max))
137 maximum_ = max;
138
139 int min_length = 0;
140 if (dict->GetInteger("minLength", &min_length) ||
141 dict->GetInteger("minItems", &min_length)) {
142 DCHECK_GE(min_length, 0);
143 min_length_ = min_length;
144 }
145
146 int max_length = 0;
147 if (dict->GetInteger("maxLength", &max_length) ||
148 dict->GetInteger("maxItems", &max_length)) {
149 DCHECK_GE(max_length, 0);
150 max_length_ = max_length;
151 }
152
153 if (type_ == ArgumentType::OBJECT) {
154 const base::DictionaryValue* properties_value = nullptr;
155 if (dict->GetDictionary("properties", &properties_value)) {
156 for (base::DictionaryValue::Iterator iter(*properties_value);
157 !iter.IsAtEnd(); iter.Advance()) {
158 properties_[iter.key()] = base::MakeUnique<ArgumentSpec>(iter.value());
159 }
160 }
161 const base::DictionaryValue* additional_properties_value = nullptr;
162 if (dict->GetDictionary("additionalProperties",
163 &additional_properties_value)) {
164 additional_properties_ =
165 base::MakeUnique<ArgumentSpec>(*additional_properties_value);
166 // Additional properties are always optional.
167 additional_properties_->optional_ = true;
168 }
169 std::string instance_of;
170 if (dict->GetString("isInstanceOf", &instance_of)) {
171 instance_of_ = instance_of;
172 }
173 } else if (type_ == ArgumentType::LIST) {
174 const base::DictionaryValue* item_value = nullptr;
175 CHECK(dict->GetDictionary("items", &item_value));
176 list_element_type_ = base::MakeUnique<ArgumentSpec>(*item_value);
177 } else if (type_ == ArgumentType::STRING) {
178 // Technically, there's no reason enums couldn't be other objects (e.g.
179 // numbers), but right now they seem to be exclusively strings. We could
180 // always update this if need be.
181 const base::ListValue* enums = nullptr;
182 if (dict->GetList("enum", &enums)) {
183 size_t size = enums->GetSize();
184 CHECK_GT(size, 0u);
185 for (size_t i = 0; i < size; ++i) {
186 std::string enum_value;
187 // Enum entries come in two versions: a list of possible strings, and
188 // a dictionary with a field 'name'.
189 if (!enums->GetString(i, &enum_value)) {
190 const base::DictionaryValue* enum_value_dictionary = nullptr;
191 CHECK(enums->GetDictionary(i, &enum_value_dictionary));
192 CHECK(enum_value_dictionary->GetString("name", &enum_value));
193 }
194 enum_values_.insert(std::move(enum_value));
195 }
196 }
197 }
198
199 // Check if we should preserve null in objects. Right now, this is only used
200 // on arguments of type object and any (in fact, it's only used in the storage
201 // API), but it could potentially make sense for lists or functions as well.
202 if (type_ == ArgumentType::OBJECT || type_ == ArgumentType::ANY)
203 dict->GetBoolean("preserveNull", &preserve_null_);
204 }
205
206 ArgumentSpec::~ArgumentSpec() {}
207
208 bool ArgumentSpec::ParseArgument(v8::Local<v8::Context> context,
209 v8::Local<v8::Value> value,
210 const APITypeReferenceMap& refs,
211 std::unique_ptr<base::Value>* out_value,
212 std::string* error) const {
213 if (type_ == ArgumentType::FUNCTION) {
214 if (!value->IsFunction()) {
215 *error = GetInvalidTypeError(value);
216 return false;
217 }
218
219 if (out_value) {
220 // Certain APIs (contextMenus) have functions as parameters other than the
221 // callback (contextMenus uses it for an onclick listener). Our generated
222 // types have adapted to consider functions "objects" and serialize them
223 // as dictionaries.
224 // TODO(devlin): It'd be awfully nice to get rid of this eccentricity.
225 *out_value = base::MakeUnique<base::DictionaryValue>();
226 }
227 return true;
228 }
229
230 if (type_ == ArgumentType::REF) {
231 DCHECK(ref_);
232 const ArgumentSpec* reference = refs.GetSpec(ref_.value());
233 DCHECK(reference) << ref_.value();
234 return reference->ParseArgument(context, value, refs, out_value, error);
235 }
236
237 if (type_ == ArgumentType::CHOICES) {
238 for (const auto& choice : choices_) {
239 if (choice->ParseArgument(context, value, refs, out_value, error))
240 return true;
241 }
242 *error = api_errors::InvalidChoice();
243 return false;
244 }
245
246 if (IsFundamentalType())
247 return ParseArgumentToFundamental(context, value, out_value, error);
248 if (type_ == ArgumentType::OBJECT) {
249 // Don't allow functions or arrays (even though they are technically
250 // objects). This is to make it easier to match otherwise-ambiguous
251 // signatures. For instance, if an API method has an optional object
252 // parameter and then an optional callback, we wouldn't necessarily be able
253 // to match the arguments if we allowed functions as objects.
254 if (!value->IsObject() || value->IsFunction() || value->IsArray()) {
255 *error = GetInvalidTypeError(value);
256 return false;
257 }
258 v8::Local<v8::Object> object = value.As<v8::Object>();
259 return ParseArgumentToObject(context, object, refs, out_value, error);
260 }
261 if (type_ == ArgumentType::LIST) {
262 if (!value->IsArray()) {
263 *error = GetInvalidTypeError(value);
264 return false;
265 }
266 v8::Local<v8::Array> array = value.As<v8::Array>();
267 return ParseArgumentToArray(context, array, refs, out_value, error);
268 }
269 if (type_ == ArgumentType::BINARY) {
270 if (!value->IsArrayBuffer() && !value->IsArrayBufferView()) {
271 *error = GetInvalidTypeError(value);
272 return false;
273 }
274 return ParseArgumentToAny(context, value, out_value, error);
275 }
276 if (type_ == ArgumentType::ANY)
277 return ParseArgumentToAny(context, value, out_value, error);
278 NOTREACHED();
279 return false;
280 }
281
282 const std::string& ArgumentSpec::GetTypeName() const {
283 if (!type_name_.empty())
284 return type_name_;
285
286 switch (type_) {
287 case ArgumentType::INTEGER:
288 type_name_ = api_errors::kTypeInteger;
289 break;
290 case ArgumentType::DOUBLE:
291 type_name_ = api_errors::kTypeDouble;
292 break;
293 case ArgumentType::BOOLEAN:
294 type_name_ = api_errors::kTypeBoolean;
295 break;
296 case ArgumentType::STRING:
297 type_name_ = api_errors::kTypeString;
298 break;
299 case ArgumentType::OBJECT:
300 type_name_ = instance_of_ ? *instance_of_ : api_errors::kTypeObject;
301 break;
302 case ArgumentType::LIST:
303 type_name_ = api_errors::kTypeList;
304 break;
305 case ArgumentType::BINARY:
306 type_name_ = api_errors::kTypeBinary;
307 break;
308 case ArgumentType::FUNCTION:
309 type_name_ = api_errors::kTypeFunction;
310 break;
311 case ArgumentType::REF:
312 type_name_ = ref_->c_str();
313 break;
314 case ArgumentType::CHOICES: {
315 std::vector<base::StringPiece> choices_strings;
316 choices_strings.reserve(choices_.size());
317 for (const auto& choice : choices_)
318 choices_strings.push_back(choice->GetTypeName());
319 type_name_ = base::StringPrintf(
320 "[%s]", base::JoinString(choices_strings, "|").c_str());
321 break;
322 }
323 case ArgumentType::ANY:
324 type_name_ = api_errors::kTypeAny;
325 break;
326 }
327 DCHECK(!type_name_.empty());
328 return type_name_;
329 }
330
331 bool ArgumentSpec::IsFundamentalType() const {
332 return type_ == ArgumentType::INTEGER || type_ == ArgumentType::DOUBLE ||
333 type_ == ArgumentType::BOOLEAN || type_ == ArgumentType::STRING;
334 }
335
336 bool ArgumentSpec::ParseArgumentToFundamental(
337 v8::Local<v8::Context> context,
338 v8::Local<v8::Value> value,
339 std::unique_ptr<base::Value>* out_value,
340 std::string* error) const {
341 DCHECK(IsFundamentalType());
342
343 switch (type_) {
344 case ArgumentType::INTEGER: {
345 if (!value->IsInt32()) {
346 *error = GetInvalidTypeError(value);
347 return false;
348 }
349 int int_val = value.As<v8::Int32>()->Value();
350 if (!CheckFundamentalBounds(int_val, minimum_, maximum_, error))
351 return false;
352 if (out_value)
353 *out_value = base::MakeUnique<base::Value>(int_val);
354 return true;
355 }
356 case ArgumentType::DOUBLE: {
357 if (!value->IsNumber()) {
358 *error = GetInvalidTypeError(value);
359 return false;
360 }
361 double double_val = value.As<v8::Number>()->Value();
362 if (!CheckFundamentalBounds(double_val, minimum_, maximum_, error))
363 return false;
364 if (out_value)
365 *out_value = base::MakeUnique<base::Value>(double_val);
366 return true;
367 }
368 case ArgumentType::STRING: {
369 if (!value->IsString()) {
370 *error = GetInvalidTypeError(value);
371 return false;
372 }
373
374 v8::Local<v8::String> v8_string = value.As<v8::String>();
375 size_t length = static_cast<size_t>(v8_string->Length());
376 if (min_length_ && length < *min_length_) {
377 *error = api_errors::TooFewStringChars(*min_length_, length);
378 return false;
379 }
380
381 if (max_length_ && length > *max_length_) {
382 *error = api_errors::TooManyStringChars(*max_length_, length);
383 return false;
384 }
385
386 // If we don't need to match enum values and don't need to convert, we're
387 // done...
388 if (!out_value && enum_values_.empty())
389 return true;
390 // ...Otherwise, we need to convert to a std::string.
391 std::string s;
392 // We already checked that this is a string, so this should never fail.
393 CHECK(gin::Converter<std::string>::FromV8(context->GetIsolate(), value,
394 &s));
395 if (!enum_values_.empty() && enum_values_.count(s) == 0) {
396 *error = api_errors::InvalidEnumValue(enum_values_);
397 return false;
398 }
399 if (out_value) {
400 // TODO(devlin): If base::Value ever takes a std::string&&, we
401 // could use std::move to construct.
402 *out_value = base::MakeUnique<base::Value>(s);
403 }
404 return true;
405 }
406 case ArgumentType::BOOLEAN: {
407 if (!value->IsBoolean()) {
408 *error = GetInvalidTypeError(value);
409 return false;
410 }
411 if (out_value) {
412 *out_value =
413 base::MakeUnique<base::Value>(value.As<v8::Boolean>()->Value());
414 }
415 return true;
416 }
417 default:
418 NOTREACHED();
419 }
420 return false;
421 }
422
423 bool ArgumentSpec::ParseArgumentToObject(
424 v8::Local<v8::Context> context,
425 v8::Local<v8::Object> object,
426 const APITypeReferenceMap& refs,
427 std::unique_ptr<base::Value>* out_value,
428 std::string* error) const {
429 DCHECK_EQ(ArgumentType::OBJECT, type_);
430 std::unique_ptr<base::DictionaryValue> result;
431 // Only construct the result if we have an |out_value| to populate.
432 if (out_value)
433 result = base::MakeUnique<base::DictionaryValue>();
434
435 v8::Local<v8::Array> own_property_names;
436 if (!object->GetOwnPropertyNames(context).ToLocal(&own_property_names)) {
437 *error = api_errors::ScriptThrewError();
438 return false;
439 }
440
441 // Track all properties we see from |properties_| to check if any are missing.
442 // Use ArgumentSpec* instead of std::string for comparison + copy efficiency.
443 std::set<const ArgumentSpec*> seen_properties;
444 uint32_t length = own_property_names->Length();
445 std::string property_error;
446 for (uint32_t i = 0; i < length; ++i) {
447 v8::Local<v8::Value> key;
448 if (!own_property_names->Get(context, i).ToLocal(&key)) {
449 *error = api_errors::ScriptThrewError();
450 return false;
451 }
452 // In JS, all keys are strings or numbers (or symbols, but those are
453 // excluded by GetOwnPropertyNames()). If you try to set anything else
454 // (e.g. an object), it is converted to a string.
455 DCHECK(key->IsString() || key->IsNumber());
456 v8::String::Utf8Value utf8_key(key);
457
458 ArgumentSpec* property_spec = nullptr;
459 auto iter = properties_.find(*utf8_key);
460 bool allow_unserializable = false;
461 if (iter != properties_.end()) {
462 property_spec = iter->second.get();
463 seen_properties.insert(property_spec);
464 } else if (additional_properties_) {
465 property_spec = additional_properties_.get();
466 // additionalProperties: {type: any} is often used to allow anything
467 // through, including things that would normally break serialization like
468 // functions, or even NaN. If the additional properties are of
469 // ArgumentType::ANY, allow anything, even if it doesn't serialize.
470 allow_unserializable = property_spec->type_ == ArgumentType::ANY;
471 } else {
472 *error = api_errors::UnexpectedProperty(*utf8_key);
473 return false;
474 }
475
476 v8::Local<v8::Value> prop_value;
477 // Fun: It's possible that a previous getter has removed the property from
478 // the object. This isn't that big of a deal, since it would only manifest
479 // in the case of some reasonably-crazy script objects, and it's probably
480 // not worth optimizing for the uncommon case to the detriment of the
481 // common (and either should be totally safe). We can always add a
482 // HasOwnProperty() check here in the future, if we desire.
483 // See also comment in ParseArgumentToArray() about passing in custom
484 // crazy values here.
485 if (!object->Get(context, key).ToLocal(&prop_value)) {
486 *error = api_errors::ScriptThrewError();
487 return false;
488 }
489
490 // Note: We don't serialize undefined, and only serialize null if it's part
491 // of the spec.
492 // TODO(devlin): This matches current behavior, but it is correct? And
493 // we treat undefined and null the same?
494 if (prop_value->IsUndefined() || prop_value->IsNull()) {
495 if (!property_spec->optional_) {
496 *error = api_errors::MissingRequiredProperty(*utf8_key);
497 return false;
498 }
499 if (preserve_null_ && prop_value->IsNull() && result) {
500 result->SetWithoutPathExpansion(*utf8_key,
501 base::MakeUnique<base::Value>());
502 }
503 continue;
504 }
505
506 std::unique_ptr<base::Value> property;
507 if (!property_spec->ParseArgument(context, prop_value, refs,
508 result ? &property : nullptr,
509 &property_error)) {
510 if (allow_unserializable)
511 continue;
512 *error = api_errors::PropertyError(*utf8_key, property_error);
513 return false;
514 }
515 if (result)
516 result->SetWithoutPathExpansion(*utf8_key, std::move(property));
517 }
518
519 for (const auto& pair : properties_) {
520 const ArgumentSpec* spec = pair.second.get();
521 if (!spec->optional_ && seen_properties.count(spec) == 0) {
522 *error = api_errors::MissingRequiredProperty(pair.first.c_str());
523 return false;
524 }
525 }
526
527 if (instance_of_) {
528 // Check for the instance somewhere in the object's prototype chain.
529 // NOTE: This only checks that something in the prototype chain was
530 // constructed with the same name as the desired instance, but doesn't
531 // validate that it's the same constructor as the expected one. For
532 // instance, if we expect isInstanceOf == 'Date', script could pass in
533 // (function() {
534 // function Date() {}
535 // return new Date();
536 // })()
537 // Since the object contains 'Date' in its prototype chain, this check
538 // succeeds, even though the object is not of built-in type Date.
539 // Since this isn't (or at least shouldn't be) a security check, this is
540 // okay.
541 bool found = false;
542 v8::Local<v8::Value> next_check = object;
543 do {
544 v8::Local<v8::Object> current = next_check.As<v8::Object>();
545 v8::String::Utf8Value constructor(current->GetConstructorName());
546 if (*instance_of_ ==
547 base::StringPiece(*constructor, constructor.length())) {
548 found = true;
549 break;
550 }
551 next_check = current->GetPrototype();
552 } while (next_check->IsObject());
553
554 if (!found) {
555 *error = api_errors::NotAnInstance(instance_of_->c_str());
556 return false;
557 }
558 }
559
560 if (out_value)
561 *out_value = std::move(result);
562 return true;
563 }
564
565 bool ArgumentSpec::ParseArgumentToArray(v8::Local<v8::Context> context,
566 v8::Local<v8::Array> value,
567 const APITypeReferenceMap& refs,
568 std::unique_ptr<base::Value>* out_value,
569 std::string* error) const {
570 DCHECK_EQ(ArgumentType::LIST, type_);
571
572 uint32_t length = value->Length();
573 if (min_length_ && length < *min_length_) {
574 *error = api_errors::TooFewArrayItems(*min_length_, length);
575 return false;
576 }
577
578 if (max_length_ && length > *max_length_) {
579 *error = api_errors::TooManyArrayItems(*max_length_, length);
580 return false;
581 }
582
583 std::unique_ptr<base::ListValue> result;
584 // Only construct the result if we have an |out_value| to populate.
585 if (out_value)
586 result = base::MakeUnique<base::ListValue>();
587
588 std::string item_error;
589 for (uint32_t i = 0; i < length; ++i) {
590 v8::MaybeLocal<v8::Value> maybe_subvalue = value->Get(context, i);
591 v8::Local<v8::Value> subvalue;
592 // Note: This can fail in the case of a developer passing in the following:
593 // var a = [];
594 // Object.defineProperty(a, 0, { get: () => { throw new Error('foo'); } });
595 // Currently, this will cause the developer-specified error ('foo') to be
596 // thrown.
597 // TODO(devlin): This is probably fine, but it's worth contemplating
598 // catching the error and throwing our own.
599 if (!maybe_subvalue.ToLocal(&subvalue))
600 return false;
601 std::unique_ptr<base::Value> item;
602 if (!list_element_type_->ParseArgument(
603 context, subvalue, refs, result ? &item : nullptr, &item_error)) {
604 *error = api_errors::IndexError(i, item_error);
605 return false;
606 }
607 if (result)
608 result->Append(std::move(item));
609 }
610 if (out_value)
611 *out_value = std::move(result);
612 return true;
613 }
614
615 bool ArgumentSpec::ParseArgumentToAny(v8::Local<v8::Context> context,
616 v8::Local<v8::Value> value,
617 std::unique_ptr<base::Value>* out_value,
618 std::string* error) const {
619 DCHECK(type_ == ArgumentType::ANY || type_ == ArgumentType::BINARY);
620 if (out_value) {
621 std::unique_ptr<content::V8ValueConverter> converter =
622 content::V8ValueConverter::Create();
623 converter->SetStripNullFromObjects(!preserve_null_);
624 std::unique_ptr<base::Value> converted =
625 converter->FromV8Value(value, context);
626 if (!converted) {
627 *error = api_errors::UnserializableValue();
628 return false;
629 }
630 if (type_ == ArgumentType::BINARY)
631 DCHECK_EQ(base::Value::Type::BINARY, converted->GetType());
632 *out_value = std::move(converted);
633 }
634 return true;
635 }
636
637 std::string ArgumentSpec::GetInvalidTypeError(
638 v8::Local<v8::Value> value) const {
639 return api_errors::InvalidType(GetTypeName().c_str(),
640 GetV8ValueTypeString(value));
641 }
642
643 } // namespace extensions
OLDNEW
« no previous file with comments | « extensions/renderer/argument_spec.h ('k') | extensions/renderer/argument_spec_builder.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698