| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/common/extensions/api/extension_api.h" | 5 #include "chrome/common/extensions/api/extension_api.h" |
| 6 | 6 |
| 7 #include <string> | 7 #include <string> |
| 8 #include <vector> | 8 #include <vector> |
| 9 | 9 |
| 10 #include "base/file_util.h" | 10 #include "base/file_util.h" |
| 11 #include "base/files/file_path.h" | 11 #include "base/files/file_path.h" |
| 12 #include "base/json/json_reader.h" | |
| 13 #include "base/json/json_writer.h" | 12 #include "base/json/json_writer.h" |
| 14 #include "base/memory/ref_counted.h" | 13 #include "base/memory/ref_counted.h" |
| 15 #include "base/memory/scoped_ptr.h" | 14 #include "base/memory/scoped_ptr.h" |
| 16 #include "base/path_service.h" | 15 #include "base/path_service.h" |
| 17 #include "base/stringprintf.h" | |
| 18 #include "base/values.h" | 16 #include "base/values.h" |
| 19 #include "chrome/common/chrome_paths.h" | 17 #include "chrome/common/chrome_paths.h" |
| 20 #include "chrome/common/extensions/extension.h" | 18 #include "chrome/common/extensions/extension.h" |
| 21 #include "chrome/common/extensions/features/api_feature.h" | |
| 22 #include "chrome/common/extensions/features/base_feature_provider.h" | |
| 23 #include "chrome/common/extensions/features/simple_feature.h" | 19 #include "chrome/common/extensions/features/simple_feature.h" |
| 24 #include "chrome/common/extensions/manifest.h" | 20 #include "chrome/common/extensions/manifest.h" |
| 25 #include "testing/gtest/include/gtest/gtest.h" | 21 #include "testing/gtest/include/gtest/gtest.h" |
| 26 | 22 |
| 27 namespace extensions { | 23 namespace extensions { |
| 28 namespace { | 24 namespace { |
| 29 | 25 |
| 30 SimpleFeature* CreateAPIFeature() { | 26 class TestFeatureProvider : public FeatureProvider { |
| 31 return new APIFeature(); | 27 public: |
| 32 } | 28 explicit TestFeatureProvider(Feature::Context context) |
| 29 : context_(context) { |
| 30 } |
| 31 |
| 32 virtual Feature* GetFeature(const std::string& name) OVERRIDE { |
| 33 SimpleFeature* result = new SimpleFeature(); |
| 34 result->set_name(name); |
| 35 result->extension_types()->insert(Manifest::TYPE_EXTENSION); |
| 36 result->GetContexts()->insert(context_); |
| 37 to_destroy_.push_back(make_linked_ptr(result)); |
| 38 return result; |
| 39 } |
| 40 |
| 41 private: |
| 42 std::vector<linked_ptr<Feature> > to_destroy_; |
| 43 Feature::Context context_; |
| 44 }; |
| 33 | 45 |
| 34 TEST(ExtensionAPI, Creation) { | 46 TEST(ExtensionAPI, Creation) { |
| 35 ExtensionAPI* shared_instance = ExtensionAPI::GetSharedInstance(); | 47 ExtensionAPI* shared_instance = ExtensionAPI::GetSharedInstance(); |
| 36 EXPECT_EQ(shared_instance, ExtensionAPI::GetSharedInstance()); | 48 EXPECT_EQ(shared_instance, ExtensionAPI::GetSharedInstance()); |
| 37 | 49 |
| 38 scoped_ptr<ExtensionAPI> new_instance( | 50 scoped_ptr<ExtensionAPI> new_instance( |
| 39 ExtensionAPI::CreateWithDefaultConfiguration()); | 51 ExtensionAPI::CreateWithDefaultConfiguration()); |
| 40 EXPECT_NE(new_instance.get(), | 52 EXPECT_NE(new_instance.get(), |
| 41 scoped_ptr<ExtensionAPI>( | 53 scoped_ptr<ExtensionAPI>( |
| 42 ExtensionAPI::CreateWithDefaultConfiguration()).get()); | 54 ExtensionAPI::CreateWithDefaultConfiguration()).get()); |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 106 EXPECT_FALSE(extension_api->IsPrivileged("app.isInstalled")); | 118 EXPECT_FALSE(extension_api->IsPrivileged("app.isInstalled")); |
| 107 EXPECT_FALSE(extension_api->IsPrivileged("storage.local")); | 119 EXPECT_FALSE(extension_api->IsPrivileged("storage.local")); |
| 108 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.onChanged")); | 120 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.onChanged")); |
| 109 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.set")); | 121 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.set")); |
| 110 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.MAX_ITEMS")); | 122 EXPECT_FALSE(extension_api->IsPrivileged("storage.local.MAX_ITEMS")); |
| 111 EXPECT_FALSE(extension_api->IsPrivileged("storage.set")); | 123 EXPECT_FALSE(extension_api->IsPrivileged("storage.set")); |
| 112 } | 124 } |
| 113 | 125 |
| 114 TEST(ExtensionAPI, IsPrivilegedFeatures) { | 126 TEST(ExtensionAPI, IsPrivilegedFeatures) { |
| 115 struct { | 127 struct { |
| 128 std::string filename; |
| 116 std::string api_full_name; | 129 std::string api_full_name; |
| 117 bool expect_is_privilged; | 130 bool expect_is_privilged; |
| 131 Feature::Context test2_contexts; |
| 118 } test_data[] = { | 132 } test_data[] = { |
| 119 { "test1", false }, | 133 { "is_privileged_features_1.json", "test", false, |
| 120 { "test1.foo", true }, | 134 Feature::UNSPECIFIED_CONTEXT }, |
| 121 { "test2", true }, | 135 { "is_privileged_features_2.json", "test", true, |
| 122 { "test2.foo", false }, | 136 Feature::UNSPECIFIED_CONTEXT }, |
| 123 { "test2.bar", false }, | 137 { "is_privileged_features_3.json", "test", false, |
| 124 { "test2.baz", true }, | 138 Feature::UNSPECIFIED_CONTEXT }, |
| 125 { "test3", false }, | 139 { "is_privileged_features_4.json", "test.bar", false, |
| 126 { "test3.foo", true }, | 140 Feature::UNSPECIFIED_CONTEXT }, |
| 127 { "test4", false } | 141 { "is_privileged_features_5.json", "test.bar", true, |
| 142 Feature::BLESSED_EXTENSION_CONTEXT }, |
| 143 { "is_privileged_features_5.json", "test.bar", false, |
| 144 Feature::UNBLESSED_EXTENSION_CONTEXT } |
| 128 }; | 145 }; |
| 129 | 146 |
| 130 base::FilePath api_features_path; | 147 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { |
| 131 PathService::Get(chrome::DIR_TEST_DATA, &api_features_path); | 148 base::FilePath manifest_path; |
| 132 api_features_path = api_features_path.AppendASCII("extensions") | 149 PathService::Get(chrome::DIR_TEST_DATA, &manifest_path); |
| 133 .AppendASCII("extension_api_unittest") | 150 manifest_path = manifest_path.AppendASCII("extensions") |
| 134 .AppendASCII("privileged_api_features.json"); | 151 .AppendASCII("extension_api_unittest") |
| 152 .AppendASCII(test_data[i].filename); |
| 135 | 153 |
| 136 std::string api_features_str; | 154 std::string manifest_str; |
| 137 ASSERT_TRUE(file_util::ReadFileToString( | 155 ASSERT_TRUE(file_util::ReadFileToString(manifest_path, &manifest_str)) |
| 138 api_features_path, &api_features_str)) << "privileged_api_features.json"; | 156 << test_data[i].filename; |
| 139 | 157 |
| 140 base::DictionaryValue* value = static_cast<DictionaryValue*>( | 158 ExtensionAPI api; |
| 141 base::JSONReader::Read(api_features_str)); | 159 api.RegisterSchema("test", manifest_str); |
| 142 BaseFeatureProvider api_feature_provider(*value, CreateAPIFeature); | |
| 143 | 160 |
| 144 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { | 161 TestFeatureProvider test2_provider(test_data[i].test2_contexts); |
| 145 ExtensionAPI api; | 162 if (test_data[i].test2_contexts != Feature::UNSPECIFIED_CONTEXT) { |
| 146 api.RegisterDependencyProvider("api", &api_feature_provider); | 163 api.RegisterDependencyProvider("test2", &test2_provider); |
| 164 } |
| 165 |
| 147 EXPECT_EQ(test_data[i].expect_is_privilged, | 166 EXPECT_EQ(test_data[i].expect_is_privilged, |
| 148 api.IsPrivileged(test_data[i].api_full_name)) << i; | 167 api.IsPrivileged(test_data[i].api_full_name)) << i; |
| 149 } | 168 } |
| 150 } | 169 } |
| 151 | 170 |
| 152 TEST(ExtensionAPI, APIFeatures) { | |
| 153 struct { | |
| 154 std::string api_full_name; | |
| 155 bool expect_is_available; | |
| 156 Feature::Context context; | |
| 157 GURL url; | |
| 158 } test_data[] = { | |
| 159 { "test1", false, Feature::WEB_PAGE_CONTEXT, GURL() }, | |
| 160 { "test1", true, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, | |
| 161 { "test1", true, Feature::UNBLESSED_EXTENSION_CONTEXT, GURL() }, | |
| 162 { "test1", true, Feature::CONTENT_SCRIPT_CONTEXT, GURL() }, | |
| 163 { "test2", true, Feature::WEB_PAGE_CONTEXT, GURL("http://google.com") }, | |
| 164 { "test2", false, Feature::BLESSED_EXTENSION_CONTEXT, | |
| 165 GURL("http://google.com") }, | |
| 166 { "test2.foo", false, Feature::WEB_PAGE_CONTEXT, | |
| 167 GURL("http://google.com") }, | |
| 168 { "test2.foo", true, Feature::CONTENT_SCRIPT_CONTEXT, GURL() }, | |
| 169 { "test3", false, Feature::WEB_PAGE_CONTEXT, GURL("http://google.com") }, | |
| 170 { "test3.foo", true, Feature::WEB_PAGE_CONTEXT, GURL("http://google.com") }, | |
| 171 { "test3.foo", true, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, | |
| 172 { "test4", true, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, | |
| 173 { "test4.foo", false, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, | |
| 174 { "test4.foo", false, Feature::UNBLESSED_EXTENSION_CONTEXT, GURL() }, | |
| 175 { "test4.foo.foo", true, Feature::CONTENT_SCRIPT_CONTEXT, GURL() }, | |
| 176 { "test5", true, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") }, | |
| 177 { "test5", false, Feature::WEB_PAGE_CONTEXT, GURL("http://bar.com") }, | |
| 178 { "test5.blah", true, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") }, | |
| 179 { "test5.blah", false, Feature::WEB_PAGE_CONTEXT, GURL("http://bar.com") }, | |
| 180 { "test6", false, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, | |
| 181 { "test6.foo", true, Feature::BLESSED_EXTENSION_CONTEXT, GURL() }, | |
| 182 { "test7", true, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") }, | |
| 183 { "test7.foo", false, Feature::WEB_PAGE_CONTEXT, GURL("http://bar.com") }, | |
| 184 { "test7.foo", true, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") }, | |
| 185 { "test7.bar", false, Feature::WEB_PAGE_CONTEXT, GURL("http://bar.com") }, | |
| 186 { "test7.bar", false, Feature::WEB_PAGE_CONTEXT, GURL("http://foo.com") } | |
| 187 }; | |
| 188 | |
| 189 base::FilePath api_features_path; | |
| 190 PathService::Get(chrome::DIR_TEST_DATA, &api_features_path); | |
| 191 api_features_path = api_features_path.AppendASCII("extensions") | |
| 192 .AppendASCII("extension_api_unittest") | |
| 193 .AppendASCII("api_features.json"); | |
| 194 | |
| 195 std::string api_features_str; | |
| 196 ASSERT_TRUE(file_util::ReadFileToString( | |
| 197 api_features_path, &api_features_str)) << "api_features.json"; | |
| 198 | |
| 199 base::DictionaryValue* value = static_cast<DictionaryValue*>( | |
| 200 base::JSONReader::Read(api_features_str)); | |
| 201 BaseFeatureProvider api_feature_provider(*value, CreateAPIFeature); | |
| 202 | |
| 203 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { | |
| 204 ExtensionAPI api; | |
| 205 api.RegisterDependencyProvider("api", &api_feature_provider); | |
| 206 for (base::DictionaryValue::Iterator iter(*value); !iter.IsAtEnd(); | |
| 207 iter.Advance()) { | |
| 208 if (iter.key().find(".") == std::string::npos) | |
| 209 api.RegisterSchema(iter.key(), ""); | |
| 210 } | |
| 211 | |
| 212 EXPECT_EQ(test_data[i].expect_is_available, | |
| 213 api.IsAvailable(test_data[i].api_full_name, | |
| 214 NULL, | |
| 215 test_data[i].context, | |
| 216 test_data[i].url).is_available()) << i; | |
| 217 } | |
| 218 } | |
| 219 | |
| 220 TEST(ExtensionAPI, LazyGetSchema) { | 171 TEST(ExtensionAPI, LazyGetSchema) { |
| 221 scoped_ptr<ExtensionAPI> apis(ExtensionAPI::CreateWithDefaultConfiguration()); | 172 scoped_ptr<ExtensionAPI> apis(ExtensionAPI::CreateWithDefaultConfiguration()); |
| 222 | 173 |
| 223 EXPECT_EQ(NULL, apis->GetSchema("")); | 174 EXPECT_EQ(NULL, apis->GetSchema("")); |
| 224 EXPECT_EQ(NULL, apis->GetSchema("")); | 175 EXPECT_EQ(NULL, apis->GetSchema("")); |
| 225 EXPECT_EQ(NULL, apis->GetSchema("experimental")); | 176 EXPECT_EQ(NULL, apis->GetSchema("experimental")); |
| 226 EXPECT_EQ(NULL, apis->GetSchema("experimental")); | 177 EXPECT_EQ(NULL, apis->GetSchema("experimental")); |
| 227 EXPECT_EQ(NULL, apis->GetSchema("foo")); | 178 EXPECT_EQ(NULL, apis->GetSchema("foo")); |
| 228 EXPECT_EQ(NULL, apis->GetSchema("foo")); | 179 EXPECT_EQ(NULL, apis->GetSchema("foo")); |
| 229 | 180 |
| (...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 418 std::string api_name = api->GetAPINameFromFullName(test_data[i].input, | 369 std::string api_name = api->GetAPINameFromFullName(test_data[i].input, |
| 419 &child_name); | 370 &child_name); |
| 420 EXPECT_EQ(test_data[i].api_name, api_name) << test_data[i].input; | 371 EXPECT_EQ(test_data[i].api_name, api_name) << test_data[i].input; |
| 421 EXPECT_EQ(test_data[i].child_name, child_name) << test_data[i].input; | 372 EXPECT_EQ(test_data[i].child_name, child_name) << test_data[i].input; |
| 422 } | 373 } |
| 423 } | 374 } |
| 424 | 375 |
| 425 TEST(ExtensionAPI, DefaultConfigurationFeatures) { | 376 TEST(ExtensionAPI, DefaultConfigurationFeatures) { |
| 426 scoped_ptr<ExtensionAPI> api(ExtensionAPI::CreateWithDefaultConfiguration()); | 377 scoped_ptr<ExtensionAPI> api(ExtensionAPI::CreateWithDefaultConfiguration()); |
| 427 | 378 |
| 428 SimpleFeature* bookmarks = static_cast<SimpleFeature*>( | 379 SimpleFeature* bookmarks = |
| 429 api->GetFeatureDependency("api:bookmarks")); | 380 static_cast<SimpleFeature*>(api->GetFeature("bookmarks")); |
| 430 SimpleFeature* bookmarks_create = static_cast<SimpleFeature*>( | 381 SimpleFeature* bookmarks_create = |
| 431 api->GetFeatureDependency("api:bookmarks.create")); | 382 static_cast<SimpleFeature*>(api->GetFeature("bookmarks.create")); |
| 432 | 383 |
| 433 struct { | 384 struct { |
| 434 SimpleFeature* feature; | 385 SimpleFeature* feature; |
| 435 // TODO(aa): More stuff to test over time. | 386 // TODO(aa): More stuff to test over time. |
| 436 } test_data[] = { | 387 } test_data[] = { |
| 437 { bookmarks }, | 388 { bookmarks }, |
| 438 { bookmarks_create } | 389 { bookmarks_create } |
| 439 }; | 390 }; |
| 440 | 391 |
| 441 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { | 392 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { |
| 442 SimpleFeature* feature = test_data[i].feature; | 393 SimpleFeature* feature = test_data[i].feature; |
| 443 ASSERT_TRUE(feature) << i; | 394 ASSERT_TRUE(feature) << i; |
| 444 | 395 |
| 445 EXPECT_TRUE(feature->whitelist()->empty()); | 396 EXPECT_TRUE(feature->whitelist()->empty()); |
| 446 EXPECT_TRUE(feature->extension_types()->empty()); | 397 EXPECT_TRUE(feature->extension_types()->empty()); |
| 447 | 398 |
| 448 EXPECT_EQ(1u, feature->GetContexts()->size()); | 399 EXPECT_EQ(1u, feature->GetContexts()->size()); |
| 449 EXPECT_TRUE(feature->GetContexts()->count( | 400 EXPECT_TRUE(feature->GetContexts()->count( |
| 450 Feature::BLESSED_EXTENSION_CONTEXT)); | 401 Feature::BLESSED_EXTENSION_CONTEXT)); |
| 451 | 402 |
| 452 EXPECT_EQ(Feature::UNSPECIFIED_LOCATION, feature->location()); | 403 EXPECT_EQ(Feature::UNSPECIFIED_LOCATION, feature->location()); |
| 453 EXPECT_EQ(Feature::UNSPECIFIED_PLATFORM, feature->platform()); | 404 EXPECT_EQ(Feature::UNSPECIFIED_PLATFORM, feature->platform()); |
| 454 EXPECT_EQ(0, feature->min_manifest_version()); | 405 EXPECT_EQ(0, feature->min_manifest_version()); |
| 455 EXPECT_EQ(0, feature->max_manifest_version()); | 406 EXPECT_EQ(0, feature->max_manifest_version()); |
| 456 } | 407 } |
| 457 } | 408 } |
| 458 | 409 |
| 459 TEST(ExtensionAPI, FeaturesRequireContexts) { | 410 TEST(ExtensionAPI, FeaturesRequireContexts) { |
| 460 // TODO(cduvall): Make this check API featues. | 411 scoped_ptr<base::ListValue> schema1(new base::ListValue()); |
| 461 scoped_ptr<base::DictionaryValue> api_features1(new base::DictionaryValue()); | 412 base::DictionaryValue* feature_definition = new base::DictionaryValue(); |
| 462 scoped_ptr<base::DictionaryValue> api_features2(new base::DictionaryValue()); | 413 schema1->Append(feature_definition); |
| 463 base::DictionaryValue* test1 = new base::DictionaryValue(); | 414 feature_definition->SetString("namespace", "test"); |
| 464 base::DictionaryValue* test2 = new base::DictionaryValue(); | 415 feature_definition->SetBoolean("uses_feature_system", true); |
| 416 |
| 417 scoped_ptr<base::ListValue> schema2(schema1->DeepCopy()); |
| 418 |
| 465 base::ListValue* contexts = new base::ListValue(); | 419 base::ListValue* contexts = new base::ListValue(); |
| 466 contexts->Append(new base::StringValue("content_script")); | 420 contexts->Append(new base::StringValue("content_script")); |
| 467 test1->Set("contexts", contexts); | 421 feature_definition->Set("contexts", contexts); |
| 468 api_features1->Set("test", test1); | |
| 469 api_features2->Set("test", test2); | |
| 470 | 422 |
| 471 struct { | 423 struct { |
| 472 base::DictionaryValue* api_features; | 424 base::ListValue* schema; |
| 473 bool expect_success; | 425 bool expect_success; |
| 474 } test_data[] = { | 426 } test_data[] = { |
| 475 { api_features1.get(), true }, | 427 { schema1.get(), true }, |
| 476 { api_features2.get(), false } | 428 { schema2.get(), false } |
| 477 }; | 429 }; |
| 478 | 430 |
| 431 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { |
| 432 std::string schema_source; |
| 433 base::JSONWriter::Write(test_data[i].schema, &schema_source); |
| 479 | 434 |
| 480 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { | 435 ExtensionAPI api; |
| 481 BaseFeatureProvider api_feature_provider(*test_data[i].api_features, | 436 api.RegisterSchema("test", base::StringPiece(schema_source)); |
| 482 CreateAPIFeature); | 437 |
| 483 Feature* feature = api_feature_provider.GetFeature("test"); | 438 Feature* feature = api.GetFeature("test"); |
| 484 EXPECT_EQ(test_data[i].expect_success, feature != NULL) << i; | 439 EXPECT_EQ(test_data[i].expect_success, feature != NULL) << i; |
| 485 } | 440 } |
| 486 } | 441 } |
| 487 | 442 |
| 488 static void GetDictionaryFromList(const base::DictionaryValue* schema, | 443 static void GetDictionaryFromList(const base::DictionaryValue* schema, |
| 489 const std::string& list_name, | 444 const std::string& list_name, |
| 490 const int list_index, | 445 const int list_index, |
| 491 const base::DictionaryValue** out) { | 446 const base::DictionaryValue** out) { |
| 492 const base::ListValue* list; | 447 const base::ListValue* list; |
| 493 EXPECT_TRUE(schema->GetList(list_name, &list)); | 448 EXPECT_TRUE(schema->GetList(list_name, &list)); |
| (...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 548 GetDictionaryFromList(dict, "parameters", 0, &sub_dict); | 503 GetDictionaryFromList(dict, "parameters", 0, &sub_dict); |
| 549 EXPECT_TRUE(sub_dict->GetString("$ref", &type)); | 504 EXPECT_TRUE(sub_dict->GetString("$ref", &type)); |
| 550 EXPECT_EQ("test.foo.TestType", type); | 505 EXPECT_EQ("test.foo.TestType", type); |
| 551 GetDictionaryFromList(dict, "parameters", 1, &sub_dict); | 506 GetDictionaryFromList(dict, "parameters", 1, &sub_dict); |
| 552 EXPECT_TRUE(sub_dict->GetString("$ref", &type)); | 507 EXPECT_TRUE(sub_dict->GetString("$ref", &type)); |
| 553 EXPECT_EQ("fully.qualified.Type", type); | 508 EXPECT_EQ("fully.qualified.Type", type); |
| 554 } | 509 } |
| 555 | 510 |
| 556 } // namespace | 511 } // namespace |
| 557 } // namespace extensions | 512 } // namespace extensions |
| OLD | NEW |