1/* 2 * Copyright (c) 2012 The Chromium Authors. All rights reserved. 3 * Use of this source code is governed by a BSD-style license that can be 4 * found in the LICENSE file. 5 */ 6 7#include <algorithm> 8 9#include "ppapi/native_client/src/trusted/plugin/json_manifest.h" 10 11#include <stdlib.h> 12 13#include "native_client/src/include/nacl_base.h" 14#include "native_client/src/include/nacl_macros.h" 15#include "native_client/src/include/nacl_string.h" 16#include "native_client/src/include/portability.h" 17#include "native_client/src/shared/platform/nacl_check.h" 18#include "ppapi/cpp/dev/url_util_dev.h" 19#include "ppapi/cpp/var.h" 20#include "ppapi/native_client/src/trusted/plugin/plugin_error.h" 21#include "ppapi/native_client/src/trusted/plugin/pnacl_options.h" 22#include "ppapi/native_client/src/trusted/plugin/utility.h" 23#include "third_party/jsoncpp/source/include/json/reader.h" 24 25namespace plugin { 26 27namespace { 28// Top-level section name keys 29const char* const kProgramKey = "program"; 30const char* const kInterpreterKey = "interpreter"; 31const char* const kFilesKey = "files"; 32 33// ISA Dictionary keys 34const char* const kX8632Key = "x86-32"; 35const char* const kX8664Key = "x86-64"; 36const char* const kArmKey = "arm"; 37const char* const kPortableKey = "portable"; 38 39// Url Resolution keys 40const char* const kPnaclTranslateKey = "pnacl-translate"; 41const char* const kUrlKey = "url"; 42 43// PNaCl keys 44const char* const kOptLevelKey = "optlevel"; 45 46// Sample NaCl manifest file: 47// { 48// "program": { 49// "x86-32": {"url": "myprogram_x86-32.nexe"}, 50// "x86-64": {"url": "myprogram_x86-64.nexe"}, 51// "arm": {"url": "myprogram_arm.nexe"} 52// }, 53// "interpreter": { 54// "x86-32": {"url": "interpreter_x86-32.nexe"}, 55// "x86-64": {"url": "interpreter_x86-64.nexe"}, 56// "arm": {"url": "interpreter_arm.nexe"} 57// }, 58// "files": { 59// "foo.txt": { 60// "portable": {"url": "foo.txt"} 61// }, 62// "bar.txt": { 63// "x86-32": {"url": "x86-32/bar.txt"}, 64// "portable": {"url": "bar.txt"} 65// }, 66// "libfoo.so": { 67// "x86-64" : { "url": "..." } 68// } 69// } 70// } 71 72// Sample PNaCl manifest file: 73// { 74// "program": { 75// "portable": { 76// "pnacl-translate": { 77// "url": "myprogram.pexe", 78// "optlevel": 0 79// } 80// } 81// }, 82// "files": { 83// "foo.txt": { 84// "portable": {"url": "foo.txt"} 85// }, 86// "bar.txt": { 87// "portable": {"url": "bar.txt"} 88// } 89// } 90// } 91 92// Looks up |property_name| in the vector |valid_names| with length 93// |valid_name_count|. Returns true if |property_name| is found. 94bool FindMatchingProperty(const nacl::string& property_name, 95 const char** valid_names, 96 size_t valid_name_count) { 97 for (size_t i = 0; i < valid_name_count; ++i) { 98 if (property_name == valid_names[i]) { 99 return true; 100 } 101 } 102 return false; 103} 104 105// Return true if this is a valid dictionary. Having only keys present in 106// |valid_keys| and having at least the keys in |required_keys|. 107// Error messages will be placed in |error_string|, given that the dictionary 108// was the property value of |container_key|. 109// E.g., "container_key" : dictionary 110bool IsValidDictionary(const Json::Value& dictionary, 111 const nacl::string& container_key, 112 const nacl::string& parent_key, 113 const char** valid_keys, 114 size_t valid_key_count, 115 const char** required_keys, 116 size_t required_key_count, 117 nacl::string* error_string) { 118 if (!dictionary.isObject()) { 119 nacl::stringstream error_stream; 120 error_stream << parent_key << " property '" << container_key 121 << "' is non-dictionary value '" 122 << dictionary.toStyledString() << "'."; 123 *error_string = error_stream.str(); 124 return false; 125 } 126 // Check for unknown dictionary members. 127 Json::Value::Members members = dictionary.getMemberNames(); 128 for (size_t i = 0; i < members.size(); ++i) { 129 nacl::string property_name = members[i]; 130 if (!FindMatchingProperty(property_name, 131 valid_keys, 132 valid_key_count)) { 133 // For forward compatibility, we do not prohibit other keys being in 134 // the dictionary. 135 PLUGIN_PRINTF(("WARNING: '%s' property '%s' has unknown key '%s'.\n", 136 parent_key.c_str(), 137 container_key.c_str(), property_name.c_str())); 138 } 139 } 140 // Check for required members. 141 for (size_t i = 0; i < required_key_count; ++i) { 142 if (!dictionary.isMember(required_keys[i])) { 143 nacl::stringstream error_stream; 144 error_stream << parent_key << " property '" << container_key 145 << "' does not have required key: '" 146 << required_keys[i] << "'."; 147 *error_string = error_stream.str(); 148 return false; 149 } 150 } 151 return true; 152} 153 154// Validate a "url" dictionary assuming it was resolved from container_key. 155// E.g., "container_key" : { "url": "foo.txt" } 156bool IsValidUrlSpec(const Json::Value& url_spec, 157 const nacl::string& container_key, 158 const nacl::string& parent_key, 159 const nacl::string& sandbox_isa, 160 nacl::string* error_string) { 161 static const char* kManifestUrlSpecRequired[] = { 162 kUrlKey 163 }; 164 const char** urlSpecPlusOptional; 165 size_t urlSpecPlusOptionalLength; 166 if (sandbox_isa == kPortableKey) { 167 static const char* kPnaclUrlSpecPlusOptional[] = { 168 kUrlKey, 169 kOptLevelKey, 170 }; 171 urlSpecPlusOptional = kPnaclUrlSpecPlusOptional; 172 urlSpecPlusOptionalLength = NACL_ARRAY_SIZE(kPnaclUrlSpecPlusOptional); 173 } else { 174 // URL specifications must not contain "pnacl-translate" keys. 175 // This prohibits NaCl clients from invoking PNaCl. 176 if (url_spec.isMember(kPnaclTranslateKey)) { 177 nacl::stringstream error_stream; 178 error_stream << "PNaCl-like NMF with application/x-nacl mimetype instead " 179 << "of x-pnacl mimetype (has " << kPnaclTranslateKey << ")."; 180 *error_string = error_stream.str(); 181 return false; 182 } 183 urlSpecPlusOptional = kManifestUrlSpecRequired; 184 urlSpecPlusOptionalLength = NACL_ARRAY_SIZE(kManifestUrlSpecRequired); 185 } 186 if (!IsValidDictionary(url_spec, container_key, parent_key, 187 urlSpecPlusOptional, 188 urlSpecPlusOptionalLength, 189 kManifestUrlSpecRequired, 190 NACL_ARRAY_SIZE(kManifestUrlSpecRequired), 191 error_string)) { 192 return false; 193 } 194 // Verify the correct types of the fields if they exist. 195 Json::Value url = url_spec[kUrlKey]; 196 if (!url.isString()) { 197 nacl::stringstream error_stream; 198 error_stream << parent_key << " property '" << container_key << 199 "' has non-string value '" << url.toStyledString() << 200 "' for key '" << kUrlKey << "'."; 201 *error_string = error_stream.str(); 202 return false; 203 } 204 Json::Value opt_level = url_spec[kOptLevelKey]; 205 if (!opt_level.empty() && !opt_level.isNumeric()) { 206 nacl::stringstream error_stream; 207 error_stream << parent_key << " property '" << container_key << 208 "' has non-numeric value '" << opt_level.toStyledString() << 209 "' for key '" << kOptLevelKey << "'."; 210 *error_string = error_stream.str(); 211 return false; 212 } 213 return true; 214} 215 216// Validate a "pnacl-translate" dictionary, assuming it was resolved from 217// container_key. E.g., "container_key" : { "pnacl_translate" : URLSpec } 218bool IsValidPnaclTranslateSpec(const Json::Value& pnacl_spec, 219 const nacl::string& container_key, 220 const nacl::string& parent_key, 221 const nacl::string& sandbox_isa, 222 nacl::string* error_string) { 223 static const char* kManifestPnaclSpecProperties[] = { 224 kPnaclTranslateKey 225 }; 226 if (!IsValidDictionary(pnacl_spec, container_key, parent_key, 227 kManifestPnaclSpecProperties, 228 NACL_ARRAY_SIZE(kManifestPnaclSpecProperties), 229 kManifestPnaclSpecProperties, 230 NACL_ARRAY_SIZE(kManifestPnaclSpecProperties), 231 error_string)) { 232 return false; 233 } 234 Json::Value url_spec = pnacl_spec[kPnaclTranslateKey]; 235 if (!IsValidUrlSpec(url_spec, kPnaclTranslateKey, 236 container_key, sandbox_isa, error_string)) { 237 return false; 238 } 239 return true; 240} 241 242// Validates that |dictionary| is a valid ISA dictionary. An ISA dictionary 243// is validated to have keys from within the set of recognized ISAs. Unknown 244// ISAs are allowed, but ignored and warnings are produced. It is also validated 245// that it must have an entry to match the ISA specified in |sandbox_isa| or 246// have a fallback 'portable' entry if there is no match. Returns true if 247// |dictionary| is an ISA to URL map. Sets |error_info| to something 248// descriptive if it fails. 249bool IsValidISADictionary(const Json::Value& dictionary, 250 const nacl::string& parent_key, 251 const nacl::string& sandbox_isa, 252 bool must_find_matching_entry, 253 ErrorInfo* error_info) { 254 if (error_info == NULL) return false; 255 256 // An ISA to URL dictionary has to be an object. 257 if (!dictionary.isObject()) { 258 error_info->SetReport(ERROR_MANIFEST_SCHEMA_VALIDATE, 259 nacl::string("manifest: ") + parent_key + 260 " property is not an ISA to URL dictionary"); 261 return false; 262 } 263 // Build the set of reserved ISA dictionary keys. 264 const char** isaProperties; 265 size_t isaPropertiesLength; 266 if (sandbox_isa == kPortableKey) { 267 // The known values for PNaCl ISA dictionaries in the manifest. 268 static const char* kPnaclManifestISAProperties[] = { 269 kPortableKey 270 }; 271 isaProperties = kPnaclManifestISAProperties; 272 isaPropertiesLength = NACL_ARRAY_SIZE(kPnaclManifestISAProperties); 273 } else { 274 // The known values for NaCl ISA dictionaries in the manifest. 275 static const char* kNaClManifestISAProperties[] = { 276 kX8632Key, 277 kX8664Key, 278 kArmKey, 279 // "portable" is here to allow checking that, if present, it can 280 // only refer to an URL, such as for a data file, and not to 281 // "pnacl-translate", which would cause the creation of a nexe. 282 kPortableKey 283 }; 284 isaProperties = kNaClManifestISAProperties; 285 isaPropertiesLength = NACL_ARRAY_SIZE(kNaClManifestISAProperties); 286 } 287 // Check that entries in the dictionary are structurally correct. 288 Json::Value::Members members = dictionary.getMemberNames(); 289 for (size_t i = 0; i < members.size(); ++i) { 290 nacl::string property_name = members[i]; 291 Json::Value property_value = dictionary[property_name]; 292 nacl::string error_string; 293 if (FindMatchingProperty(property_name, 294 isaProperties, 295 isaPropertiesLength)) { 296 // For NaCl, arch entries can only be 297 // "arch/portable" : URLSpec 298 // For PNaCl arch in "program" dictionary entries can only be 299 // "portable" : { "pnacl-translate": URLSpec } 300 // For PNaCl arch elsewhere, dictionary entries can only be 301 // "portable" : URLSpec 302 if ((sandbox_isa != kPortableKey && 303 !IsValidUrlSpec(property_value, property_name, parent_key, 304 sandbox_isa, &error_string)) || 305 (sandbox_isa == kPortableKey && 306 parent_key == kProgramKey && 307 !IsValidPnaclTranslateSpec(property_value, property_name, parent_key, 308 sandbox_isa, &error_string)) || 309 (sandbox_isa == kPortableKey && 310 parent_key != kProgramKey && 311 !IsValidUrlSpec(property_value, property_name, parent_key, 312 sandbox_isa, &error_string))) { 313 error_info->SetReport(ERROR_MANIFEST_SCHEMA_VALIDATE, 314 nacl::string("manifest: ") + error_string); 315 return false; 316 } 317 } else { 318 // For forward compatibility, we do not prohibit other keys being in 319 // the dictionary, as they may be architectures supported in later 320 // versions. However, the value of these entries must be an URLSpec. 321 PLUGIN_PRINTF(("IsValidISADictionary: unrecognized key '%s'.\n", 322 property_name.c_str())); 323 if (!IsValidUrlSpec(property_value, property_name, parent_key, 324 sandbox_isa, &error_string)) { 325 error_info->SetReport(ERROR_MANIFEST_SCHEMA_VALIDATE, 326 nacl::string("manifest: ") + error_string); 327 return false; 328 } 329 } 330 } 331 332 if (sandbox_isa == kPortableKey) { 333 bool has_portable = dictionary.isMember(kPortableKey); 334 335 if (!has_portable) { 336 error_info->SetReport( 337 ERROR_MANIFEST_PROGRAM_MISSING_ARCH, 338 nacl::string("manifest: no version of ") + parent_key + 339 " given for portable."); 340 return false; 341 } 342 } else if (must_find_matching_entry) { 343 // TODO(elijahtaylor) add ISA resolver here if we expand ISAs to include 344 // micro-architectures that can resolve to multiple valid sandboxes. 345 bool has_isa = dictionary.isMember(sandbox_isa); 346 bool has_portable = dictionary.isMember(kPortableKey); 347 348 if (!has_isa && !has_portable) { 349 error_info->SetReport( 350 ERROR_MANIFEST_PROGRAM_MISSING_ARCH, 351 nacl::string("manifest: no version of ") + parent_key + 352 " given for current arch and no portable version found."); 353 return false; 354 } 355 } 356 357 return true; 358} 359 360void GrabUrlAndPnaclOptions(const Json::Value& url_spec, 361 nacl::string* url, 362 PnaclOptions* pnacl_options) { 363 *url = url_spec[kUrlKey].asString(); 364 if (url_spec.isMember(kOptLevelKey)) { 365 int32_t opt_raw = url_spec[kOptLevelKey].asInt(); 366 // set_opt_level will normalize the values. 367 pnacl_options->set_opt_level(opt_raw); 368 } 369} 370 371bool GetURLFromISADictionary(const Json::Value& dictionary, 372 const nacl::string& parent_key, 373 const nacl::string& sandbox_isa, 374 nacl::string* url, 375 PnaclOptions* pnacl_options, 376 ErrorInfo* error_info) { 377 if (url == NULL || pnacl_options == NULL || error_info == NULL) 378 return false; 379 380 // When the application actually requests a resolved URL, we must have 381 // a matching entry (sandbox_isa or portable) for NaCl. 382 if (!IsValidISADictionary(dictionary, parent_key, sandbox_isa, true, 383 error_info)) { 384 error_info->SetReport(ERROR_MANIFEST_RESOLVE_URL, 385 "architecture " + sandbox_isa + 386 " is not found for file " + parent_key); 387 return false; 388 } 389 390 *url = ""; 391 392 // The call to IsValidISADictionary() above guarantees that either 393 // sandbox_isa or kPortableKey is present in the dictionary. 394 bool has_portable = dictionary.isMember(kPortableKey); 395 bool has_isa = dictionary.isMember(sandbox_isa); 396 nacl::string chosen_isa; 397 if ((sandbox_isa == kPortableKey) || (has_portable && !has_isa)) { 398 chosen_isa = kPortableKey; 399 } else { 400 chosen_isa = sandbox_isa; 401 } 402 const Json::Value& isa_spec = dictionary[chosen_isa]; 403 // Check if this requires a pnacl-translate, otherwise just grab the URL. 404 // We may have pnacl-translate for isa-specific bitcode for CPU tuning. 405 if (isa_spec.isMember(kPnaclTranslateKey)) { 406 // PNaCl 407 GrabUrlAndPnaclOptions(isa_spec[kPnaclTranslateKey], url, pnacl_options); 408 pnacl_options->set_translate(true); 409 } else { 410 // NaCl 411 *url = isa_spec[kUrlKey].asString(); 412 pnacl_options->set_translate(false); 413 } 414 415 return true; 416} 417 418bool GetKeyUrl(const Json::Value& dictionary, 419 const nacl::string& key, 420 const nacl::string& sandbox_isa, 421 const Manifest* manifest, 422 nacl::string* full_url, 423 PnaclOptions* pnacl_options, 424 ErrorInfo* error_info) { 425 CHECK(full_url != NULL && error_info != NULL); 426 if (!dictionary.isMember(key)) { 427 error_info->SetReport(ERROR_MANIFEST_RESOLVE_URL, 428 "file key not found in manifest"); 429 return false; 430 } 431 const Json::Value& isa_dict = dictionary[key]; 432 nacl::string relative_url; 433 if (!GetURLFromISADictionary(isa_dict, key, sandbox_isa, &relative_url, 434 pnacl_options, error_info)) { 435 return false; 436 } 437 return manifest->ResolveURL(relative_url, full_url, error_info); 438} 439 440} // namespace 441 442bool JsonManifest::Init(const nacl::string& manifest_json, 443 ErrorInfo* error_info) { 444 if (error_info == NULL) { 445 return false; 446 } 447 Json::Reader reader; 448 if (!reader.parse(manifest_json, dictionary_)) { 449 std::string json_error = reader.getFormatedErrorMessages(); 450 error_info->SetReport(ERROR_MANIFEST_PARSING, 451 "manifest JSON parsing failed: " + json_error); 452 return false; 453 } 454 // Parse has ensured the string was valid JSON. Check that it matches the 455 // manifest schema. 456 return MatchesSchema(error_info); 457} 458 459bool JsonManifest::MatchesSchema(ErrorInfo* error_info) { 460 pp::Var exception; 461 if (error_info == NULL) { 462 return false; 463 } 464 if (!dictionary_.isObject()) { 465 error_info->SetReport( 466 ERROR_MANIFEST_SCHEMA_VALIDATE, 467 "manifest: is not a json dictionary."); 468 return false; 469 } 470 Json::Value::Members members = dictionary_.getMemberNames(); 471 for (size_t i = 0; i < members.size(); ++i) { 472 // The top level dictionary entries valid in the manifest file. 473 static const char* kManifestTopLevelProperties[] = { kProgramKey, 474 kInterpreterKey, 475 kFilesKey }; 476 nacl::string property_name = members[i]; 477 if (!FindMatchingProperty(property_name, 478 kManifestTopLevelProperties, 479 NACL_ARRAY_SIZE(kManifestTopLevelProperties))) { 480 PLUGIN_PRINTF(("JsonManifest::MatchesSchema: WARNING: unknown top-level " 481 "section '%s' in manifest.\n", property_name.c_str())); 482 } 483 } 484 485 // A manifest file must have a program section. 486 if (!dictionary_.isMember(kProgramKey)) { 487 error_info->SetReport( 488 ERROR_MANIFEST_SCHEMA_VALIDATE, 489 nacl::string("manifest: missing '") + kProgramKey + "' section."); 490 return false; 491 } 492 493 // Validate the program section. 494 // There must be a matching (portable or sandbox_isa_) entry for program for 495 // NaCl. 496 if (!IsValidISADictionary(dictionary_[kProgramKey], 497 kProgramKey, 498 sandbox_isa_, 499 true, 500 error_info)) { 501 return false; 502 } 503 504 // Validate the interpreter section (if given). 505 // There must be a matching (portable or sandbox_isa_) entry for interpreter 506 // for NaCl. 507 if (dictionary_.isMember(kInterpreterKey)) { 508 if (!IsValidISADictionary(dictionary_[kInterpreterKey], 509 kInterpreterKey, 510 sandbox_isa_, 511 true, 512 error_info)) { 513 return false; 514 } 515 } 516 517 // Validate the file dictionary (if given). 518 // The "files" key does not require a matching (portable or sandbox_isa_) 519 // entry at schema validation time for NaCl. This allows manifests to specify 520 // resources that are only loaded for a particular sandbox_isa. 521 if (dictionary_.isMember(kFilesKey)) { 522 const Json::Value& files = dictionary_[kFilesKey]; 523 if (!files.isObject()) { 524 error_info->SetReport( 525 ERROR_MANIFEST_SCHEMA_VALIDATE, 526 nacl::string("manifest: '") + kFilesKey + "' is not a dictionary."); 527 } 528 Json::Value::Members members = files.getMemberNames(); 529 for (size_t i = 0; i < members.size(); ++i) { 530 nacl::string file_name = members[i]; 531 if (!IsValidISADictionary(files[file_name], 532 file_name, 533 sandbox_isa_, 534 false, 535 error_info)) { 536 return false; 537 } 538 } 539 } 540 541 return true; 542} 543 544bool JsonManifest::ResolveURL(const nacl::string& relative_url, 545 nacl::string* full_url, 546 ErrorInfo* error_info) const { 547 // The contents of the manifest are resolved relative to the manifest URL. 548 CHECK(url_util_ != NULL); 549 pp::Var resolved_url = 550 url_util_->ResolveRelativeToURL(pp::Var(manifest_base_url_), 551 relative_url); 552 if (!resolved_url.is_string()) { 553 error_info->SetReport( 554 ERROR_MANIFEST_RESOLVE_URL, 555 "could not resolve url '" + relative_url + 556 "' relative to manifest base url '" + manifest_base_url_.c_str() + 557 "'."); 558 return false; 559 } 560 *full_url = resolved_url.AsString(); 561 return true; 562} 563 564bool JsonManifest::GetProgramURL(nacl::string* full_url, 565 PnaclOptions* pnacl_options, 566 ErrorInfo* error_info) const { 567 if (full_url == NULL || pnacl_options == NULL || error_info == NULL) 568 return false; 569 570 Json::Value program = dictionary_[kProgramKey]; 571 572 nacl::string nexe_url; 573 nacl::string error_string; 574 575 if (!GetURLFromISADictionary(program, 576 kProgramKey, 577 sandbox_isa_, 578 &nexe_url, 579 pnacl_options, 580 error_info)) { 581 return false; 582 } 583 584 return ResolveURL(nexe_url, full_url, error_info); 585} 586 587bool JsonManifest::GetFileKeys(std::set<nacl::string>* keys) const { 588 if (!dictionary_.isMember(kFilesKey)) { 589 // trivial success: no keys when there is no "files" section. 590 return true; 591 } 592 const Json::Value& files = dictionary_[kFilesKey]; 593 CHECK(files.isObject()); 594 Json::Value::Members members = files.getMemberNames(); 595 for (size_t i = 0; i < members.size(); ++i) { 596 keys->insert(members[i]); 597 } 598 return true; 599} 600 601bool JsonManifest::ResolveKey(const nacl::string& key, 602 nacl::string* full_url, 603 PnaclOptions* pnacl_options, 604 ErrorInfo* error_info) const { 605 NaClLog(3, "JsonManifest::ResolveKey(%s)\n", key.c_str()); 606 // key must be one of kProgramKey or kFileKey '/' file-section-key 607 608 if (full_url == NULL || pnacl_options == NULL || error_info == NULL) 609 return false; 610 611 if (key == kProgramKey) { 612 return GetKeyUrl(dictionary_, key, sandbox_isa_, this, full_url, 613 pnacl_options, error_info); 614 } 615 nacl::string::const_iterator p = find(key.begin(), key.end(), '/'); 616 if (p == key.end()) { 617 error_info->SetReport(ERROR_MANIFEST_RESOLVE_URL, 618 nacl::string("ResolveKey: invalid key, no slash: ") 619 + key); 620 return false; 621 } 622 623 // generalize to permit other sections? 624 nacl::string prefix(key.begin(), p); 625 if (prefix != kFilesKey) { 626 error_info->SetReport(ERROR_MANIFEST_RESOLVE_URL, 627 nacl::string("ResolveKey: invalid key: not \"files\"" 628 " prefix: ") + key); 629 return false; 630 } 631 632 nacl::string rest(p + 1, key.end()); 633 634 const Json::Value& files = dictionary_[kFilesKey]; 635 if (!files.isObject()) { 636 error_info->SetReport( 637 ERROR_MANIFEST_RESOLVE_URL, 638 nacl::string("ResolveKey: no \"files\" dictionary")); 639 return false; 640 } 641 if (!files.isMember(rest)) { 642 error_info->SetReport( 643 ERROR_MANIFEST_RESOLVE_URL, 644 nacl::string("ResolveKey: no such \"files\" entry: ") + key); 645 return false; 646 } 647 return GetKeyUrl(files, rest, sandbox_isa_, this, full_url, pnacl_options, 648 error_info); 649} 650 651} // namespace plugin 652