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
3 // found in the LICENSE file.
5 #include "chrome/common/extensions/extension_file_util.h"
7 #include "base/file_util.h"
8 #include "base/files/scoped_temp_dir.h"
9 #include "base/json/json_string_value_serializer.h"
10 #include "base/path_service.h"
11 #include "base/strings/stringprintf.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/common/chrome_paths.h"
14 #include "extensions/common/constants.h"
15 #include "extensions/common/extension.h"
16 #include "extensions/common/manifest.h"
17 #include "extensions/common/manifest_constants.h"
18 #include "grit/generated_resources.h"
19 #include "testing/gmock/include/gmock/gmock.h"
20 #include "testing/gtest/include/gtest/gtest.h"
21 #include "ui/base/l10n/l10n_util.h"
23 using extensions::Extension
;
24 using extensions::Manifest
;
26 namespace keys
= extensions::manifest_keys
;
28 class ExtensionFileUtilTest
: public testing::Test
{
31 TEST_F(ExtensionFileUtilTest
, InstallUninstallGarbageCollect
) {
32 base::ScopedTempDir temp
;
33 ASSERT_TRUE(temp
.CreateUniqueTempDir());
35 // Create a source extension.
36 std::string
extension_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
37 std::string
version("1.0");
38 base::FilePath src
= temp
.path().AppendASCII(extension_id
);
39 ASSERT_TRUE(base::CreateDirectory(src
));
41 // Create a extensions tree.
42 base::FilePath all_extensions
= temp
.path().AppendASCII("extensions");
43 ASSERT_TRUE(base::CreateDirectory(all_extensions
));
45 // Install in empty directory. Should create parent directories as needed.
46 base::FilePath version_1
= extension_file_util::InstallExtension(
51 ASSERT_EQ(version_1
.value(),
52 all_extensions
.AppendASCII(extension_id
).AppendASCII("1.0_0")
54 ASSERT_TRUE(base::DirectoryExists(version_1
));
56 // Should have moved the source.
57 ASSERT_FALSE(base::DirectoryExists(src
));
59 // Install again. Should create a new one with different name.
60 ASSERT_TRUE(base::CreateDirectory(src
));
61 base::FilePath version_2
= extension_file_util::InstallExtension(
66 ASSERT_EQ(version_2
.value(),
67 all_extensions
.AppendASCII(extension_id
).AppendASCII("1.0_1")
69 ASSERT_TRUE(base::DirectoryExists(version_2
));
71 // Should have moved the source.
72 ASSERT_FALSE(base::DirectoryExists(src
));
74 // Install yet again. Should create a new one with a different name.
75 ASSERT_TRUE(base::CreateDirectory(src
));
76 base::FilePath version_3
= extension_file_util::InstallExtension(
81 ASSERT_EQ(version_3
.value(),
82 all_extensions
.AppendASCII(extension_id
).AppendASCII("1.0_2")
84 ASSERT_TRUE(base::DirectoryExists(version_3
));
86 // Collect garbage. Should remove first one.
87 std::multimap
<std::string
, base::FilePath
> extension_paths
;
88 extension_paths
.insert(std::make_pair(extension_id
,
89 base::FilePath().AppendASCII(extension_id
).Append(version_2
.BaseName())));
90 extension_paths
.insert(std::make_pair(extension_id
,
91 base::FilePath().AppendASCII(extension_id
).Append(version_3
.BaseName())));
92 extension_file_util::GarbageCollectExtensions(all_extensions
,
95 ASSERT_FALSE(base::DirectoryExists(version_1
));
96 ASSERT_TRUE(base::DirectoryExists(version_2
));
97 ASSERT_TRUE(base::DirectoryExists(version_3
));
99 // Uninstall. Should remove entire extension subtree.
100 extension_file_util::UninstallExtension(all_extensions
, extension_id
);
101 ASSERT_FALSE(base::DirectoryExists(version_2
.DirName()));
102 ASSERT_FALSE(base::DirectoryExists(version_3
.DirName()));
103 ASSERT_TRUE(base::DirectoryExists(all_extensions
));
106 TEST_F(ExtensionFileUtilTest
, LoadExtensionWithValidLocales
) {
107 base::FilePath install_dir
;
108 ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA
, &install_dir
));
109 install_dir
= install_dir
.AppendASCII("extensions")
111 .AppendASCII("Extensions")
112 .AppendASCII("behllobkkfkfnphdnhnkndlbkcpglgmj")
113 .AppendASCII("1.0.0.0");
116 scoped_refptr
<Extension
> extension(extension_file_util::LoadExtension(
117 install_dir
, Manifest::UNPACKED
, Extension::NO_FLAGS
, &error
));
118 ASSERT_TRUE(extension
.get() != NULL
);
119 EXPECT_EQ("The first extension that I made.", extension
->description());
122 TEST_F(ExtensionFileUtilTest
, LoadExtensionWithoutLocalesFolder
) {
123 base::FilePath install_dir
;
124 ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA
, &install_dir
));
125 install_dir
= install_dir
.AppendASCII("extensions")
127 .AppendASCII("Extensions")
128 .AppendASCII("bjafgdebaacbbbecmhlhpofkepfkgcpa")
132 scoped_refptr
<Extension
> extension(extension_file_util::LoadExtension(
133 install_dir
, Manifest::UNPACKED
, Extension::NO_FLAGS
, &error
));
134 ASSERT_FALSE(extension
.get() == NULL
);
135 EXPECT_TRUE(error
.empty());
138 TEST_F(ExtensionFileUtilTest
, CheckIllegalFilenamesNoUnderscores
) {
139 base::ScopedTempDir temp
;
140 ASSERT_TRUE(temp
.CreateUniqueTempDir());
142 base::FilePath src_path
= temp
.path().AppendASCII("some_dir");
143 ASSERT_TRUE(base::CreateDirectory(src_path
));
145 std::string data
= "{ \"name\": { \"message\": \"foobar\" } }";
146 ASSERT_TRUE(file_util::WriteFile(src_path
.AppendASCII("some_file.txt"),
147 data
.c_str(), data
.length()));
149 EXPECT_TRUE(extension_file_util::CheckForIllegalFilenames(temp
.path(),
153 TEST_F(ExtensionFileUtilTest
, CheckIllegalFilenamesOnlyReserved
) {
154 base::ScopedTempDir temp
;
155 ASSERT_TRUE(temp
.CreateUniqueTempDir());
157 const base::FilePath::CharType
* folders
[] =
158 { extensions::kLocaleFolder
, extensions::kPlatformSpecificFolder
};
160 for (size_t i
= 0; i
< arraysize(folders
); i
++) {
161 base::FilePath src_path
= temp
.path().Append(folders
[i
]);
162 ASSERT_TRUE(base::CreateDirectory(src_path
));
166 EXPECT_TRUE(extension_file_util::CheckForIllegalFilenames(temp
.path(),
170 TEST_F(ExtensionFileUtilTest
, CheckIllegalFilenamesReservedAndIllegal
) {
171 base::ScopedTempDir temp
;
172 ASSERT_TRUE(temp
.CreateUniqueTempDir());
174 base::FilePath src_path
= temp
.path().Append(extensions::kLocaleFolder
);
175 ASSERT_TRUE(base::CreateDirectory(src_path
));
177 src_path
= temp
.path().AppendASCII("_some_dir");
178 ASSERT_TRUE(base::CreateDirectory(src_path
));
181 EXPECT_FALSE(extension_file_util::CheckForIllegalFilenames(temp
.path(),
185 TEST_F(ExtensionFileUtilTest
,
186 LoadExtensionGivesHelpfullErrorOnMissingManifest
) {
187 base::FilePath install_dir
;
188 ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA
, &install_dir
));
189 install_dir
= install_dir
.AppendASCII("extensions")
191 .AppendASCII("Extensions")
192 .AppendASCII("dddddddddddddddddddddddddddddddd")
196 scoped_refptr
<Extension
> extension(extension_file_util::LoadExtension(
197 install_dir
, Manifest::UNPACKED
, Extension::NO_FLAGS
, &error
));
198 ASSERT_TRUE(extension
.get() == NULL
);
199 ASSERT_FALSE(error
.empty());
200 ASSERT_STREQ("Manifest file is missing or unreadable.", error
.c_str());
203 TEST_F(ExtensionFileUtilTest
, LoadExtensionGivesHelpfullErrorOnBadManifest
) {
204 base::FilePath install_dir
;
205 ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA
, &install_dir
));
206 install_dir
= install_dir
.AppendASCII("extensions")
208 .AppendASCII("Extensions")
209 .AppendASCII("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
213 scoped_refptr
<Extension
> extension(extension_file_util::LoadExtension(
214 install_dir
, Manifest::UNPACKED
, Extension::NO_FLAGS
, &error
));
215 ASSERT_TRUE(extension
.get() == NULL
);
216 ASSERT_FALSE(error
.empty());
217 ASSERT_STREQ("Manifest is not valid JSON. "
218 "Line: 2, column: 16, Syntax error.", error
.c_str());
221 TEST_F(ExtensionFileUtilTest
, FailLoadingNonUTF8Scripts
) {
222 base::FilePath install_dir
;
223 ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA
, &install_dir
));
224 install_dir
= install_dir
.AppendASCII("extensions")
226 .AppendASCII("bad_encoding");
229 scoped_refptr
<Extension
> extension(extension_file_util::LoadExtension(
230 install_dir
, Manifest::UNPACKED
, Extension::NO_FLAGS
, &error
));
231 ASSERT_TRUE(extension
.get() == NULL
);
232 ASSERT_STREQ("Could not load file 'bad_encoding.js' for content script. "
233 "It isn't UTF-8 encoded.",
237 static scoped_refptr
<Extension
> LoadExtensionManifest(
238 base::DictionaryValue
* manifest
,
239 const base::FilePath
& manifest_dir
,
240 Manifest::Location location
,
242 std::string
* error
) {
243 scoped_refptr
<Extension
> extension
= Extension::Create(
244 manifest_dir
, location
, *manifest
, extra_flags
, error
);
248 static scoped_refptr
<Extension
> LoadExtensionManifest(
249 const std::string
& manifest_value
,
250 const base::FilePath
& manifest_dir
,
251 Manifest::Location location
,
253 std::string
* error
) {
254 JSONStringValueSerializer
serializer(manifest_value
);
255 scoped_ptr
<base::Value
> result(serializer
.Deserialize(NULL
, error
));
258 CHECK_EQ(base::Value::TYPE_DICTIONARY
, result
->GetType());
259 return LoadExtensionManifest(
260 static_cast<base::DictionaryValue
*>(result
.get()),
267 TEST_F(ExtensionFileUtilTest
, ValidateThemeUTF8
) {
268 base::ScopedTempDir temp
;
269 ASSERT_TRUE(temp
.CreateUniqueTempDir());
271 // "aeo" with accents. Use http://0xcc.net/jsescape/ to decode them.
272 std::string non_ascii_file
= "\xC3\xA0\xC3\xA8\xC3\xB2.png";
273 base::FilePath non_ascii_path
=
274 temp
.path().Append(base::FilePath::FromUTF8Unsafe(non_ascii_file
));
275 file_util::WriteFile(non_ascii_path
, "", 0);
277 std::string kManifest
=
279 "{ \"name\": \"Test\", \"version\": \"1.0\", "
280 " \"theme\": { \"images\": { \"theme_frame\": \"%s\" } }"
281 "}", non_ascii_file
.c_str());
283 scoped_refptr
<Extension
> extension
= LoadExtensionManifest(
284 kManifest
, temp
.path(), Manifest::UNPACKED
, 0, &error
);
285 ASSERT_TRUE(extension
.get()) << error
;
287 std::vector
<extensions::InstallWarning
> warnings
;
288 EXPECT_TRUE(extension_file_util::ValidateExtension(
289 extension
.get(), &error
, &warnings
)) << error
;
290 EXPECT_EQ(0U, warnings
.size());
293 TEST_F(ExtensionFileUtilTest
, BackgroundScriptsMustExist
) {
294 base::ScopedTempDir temp
;
295 ASSERT_TRUE(temp
.CreateUniqueTempDir());
297 scoped_ptr
<base::DictionaryValue
> value(new base::DictionaryValue());
298 value
->SetString("name", "test");
299 value
->SetString("version", "1");
300 value
->SetInteger("manifest_version", 1);
302 base::ListValue
* scripts
= new base::ListValue();
303 scripts
->Append(new base::StringValue("foo.js"));
304 value
->Set("background.scripts", scripts
);
307 std::vector
<extensions::InstallWarning
> warnings
;
308 scoped_refptr
<Extension
> extension
= LoadExtensionManifest(
309 value
.get(), temp
.path(), Manifest::UNPACKED
, 0, &error
);
310 ASSERT_TRUE(extension
.get()) << error
;
312 EXPECT_FALSE(extension_file_util::ValidateExtension(
313 extension
.get(), &error
, &warnings
));
314 EXPECT_EQ(l10n_util::GetStringFUTF8(
315 IDS_EXTENSION_LOAD_BACKGROUND_SCRIPT_FAILED
,
316 base::ASCIIToUTF16("foo.js")),
318 EXPECT_EQ(0U, warnings
.size());
321 scripts
->Append(new base::StringValue("http://google.com/foo.js"));
323 extension
= LoadExtensionManifest(value
.get(), temp
.path(),
324 Manifest::UNPACKED
, 0, &error
);
325 ASSERT_TRUE(extension
.get()) << error
;
328 EXPECT_FALSE(extension_file_util::ValidateExtension(
329 extension
.get(), &error
, &warnings
));
330 EXPECT_EQ(l10n_util::GetStringFUTF8(
331 IDS_EXTENSION_LOAD_BACKGROUND_SCRIPT_FAILED
,
332 base::ASCIIToUTF16("http://google.com/foo.js")),
334 EXPECT_EQ(0U, warnings
.size());
337 // Private key, generated by Chrome specifically for this test, and
338 // never used elsewhere.
339 const char private_key
[] =
340 "-----BEGIN PRIVATE KEY-----\n"
341 "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKt02SR0FYaYy6fpW\n"
342 "MAA+kU1BgK3d+OmmWfdr+JATIjhRkyeSF4lTd/71JQsyKqPzYkQPi3EeROWM+goTv\n"
343 "EhJqq07q63BolpsFmlV+S4ny+sBA2B4aWwRYXlBWikdrQSA0mJMzvEHc6nKzBgXik\n"
344 "QSVbyyBNAsxlDB9WaCxRVOpK3AgMBAAECgYBGvSPlrVtAOAQ2V8j9FqorKZA8SLPX\n"
345 "IeJC/yzU3RB2nPMjI17aMOvrUHxJUhzMeh4jwabVvSzzDtKFozPGupW3xaI8sQdi2\n"
346 "WWMTQIk/Q9HHDWoQ9qA6SwX2qWCc5SyjCKqVp78ye+000kqTJYjBsDgXeAlzKcx2B\n"
347 "4GAAeWonDdkQJBANNb8wrqNWFn7DqyQTfELzcRTRnqQ/r1pdeJo6obzbnwGnlqe3t\n"
348 "KhLjtJNIGrQg5iC0OVLWFuvPJs0t3z62A1ckCQQDPq2JZuwTwu5Pl4DJ0r9O1FdqN\n"
349 "JgqPZyMptokCDQ3khLLGakIu+TqB9YtrzI69rJMSG2Egb+6McaDX+dh3XmR/AkB9t\n"
350 "xJf6qDnmA2td/tMtTc0NOk8Qdg/fD8xbZ/YfYMnVoYYs9pQoilBaWRePDRNURMLYZ\n"
351 "vHAI0Llmw7tj7jv17pAkEAz44uXRpjRKtllUIvi5pUENAHwDz+HvdpGH68jpU3hmb\n"
352 "uOwrmnQYxaMReFV68Z2w9DcLZn07f7/R9Wn72z89CxwJAFsDoNaDes4h48bX7plct\n"
353 "s9ACjmTwcCigZjN2K7AGv7ntCLF3DnV5dK0dTHNaAdD3SbY3jl29Rk2CwiURSX6Ee\n"
355 "-----END PRIVATE KEY-----\n";
357 TEST_F(ExtensionFileUtilTest
, FindPrivateKeyFiles
) {
358 base::ScopedTempDir temp
;
359 ASSERT_TRUE(temp
.CreateUniqueTempDir());
361 base::FilePath src_path
= temp
.path().AppendASCII("some_dir");
362 ASSERT_TRUE(base::CreateDirectory(src_path
));
364 ASSERT_TRUE(file_util::WriteFile(src_path
.AppendASCII("a_key.pem"),
365 private_key
, arraysize(private_key
)));
366 ASSERT_TRUE(file_util::WriteFile(src_path
.AppendASCII("second_key.pem"),
367 private_key
, arraysize(private_key
)));
368 // Shouldn't find a key with a different extension.
369 ASSERT_TRUE(file_util::WriteFile(src_path
.AppendASCII("key.diff_ext"),
370 private_key
, arraysize(private_key
)));
371 // Shouldn't find a key that isn't parsable.
372 ASSERT_TRUE(file_util::WriteFile(src_path
.AppendASCII("unparsable_key.pem"),
373 private_key
, arraysize(private_key
) - 30));
374 std::vector
<base::FilePath
> private_keys
=
375 extension_file_util::FindPrivateKeyFiles(temp
.path());
376 EXPECT_EQ(2U, private_keys
.size());
377 EXPECT_THAT(private_keys
,
378 testing::Contains(src_path
.AppendASCII("a_key.pem")));
379 EXPECT_THAT(private_keys
,
380 testing::Contains(src_path
.AppendASCII("second_key.pem")));
383 TEST_F(ExtensionFileUtilTest
, WarnOnPrivateKey
) {
384 base::ScopedTempDir temp
;
385 ASSERT_TRUE(temp
.CreateUniqueTempDir());
387 base::FilePath ext_path
= temp
.path().AppendASCII("ext_root");
388 ASSERT_TRUE(base::CreateDirectory(ext_path
));
390 const char manifest
[] =
392 " \"name\": \"Test Extension\",\n"
393 " \"version\": \"1.0\",\n"
394 " \"manifest_version\": 2,\n"
395 " \"description\": \"The first extension that I made.\"\n"
397 ASSERT_TRUE(file_util::WriteFile(ext_path
.AppendASCII("manifest.json"),
398 manifest
, strlen(manifest
)));
399 ASSERT_TRUE(file_util::WriteFile(ext_path
.AppendASCII("a_key.pem"),
400 private_key
, strlen(private_key
)));
403 scoped_refptr
<Extension
> extension(extension_file_util::LoadExtension(
404 ext_path
, "the_id", Manifest::EXTERNAL_PREF
,
405 Extension::NO_FLAGS
, &error
));
406 ASSERT_TRUE(extension
.get()) << error
;
407 ASSERT_EQ(1u, extension
->install_warnings().size());
409 extension
->install_warnings(),
410 testing::ElementsAre(
412 &extensions::InstallWarning::message
,
413 testing::ContainsRegex(
414 "extension includes the key file.*ext_root.a_key.pem"))));
416 // Turn the warning into an error with ERROR_ON_PRIVATE_KEY.
417 extension
= extension_file_util::LoadExtension(
418 ext_path
, "the_id", Manifest::EXTERNAL_PREF
,
419 Extension::ERROR_ON_PRIVATE_KEY
, &error
);
420 EXPECT_FALSE(extension
.get());
422 testing::ContainsRegex(
423 "extension includes the key file.*ext_root.a_key.pem"));
426 TEST_F(ExtensionFileUtilTest
, CheckZeroLengthImageFile
) {
427 base::FilePath install_dir
;
428 ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA
, &install_dir
));
430 // Try to install an extension with a zero-length icon file.
431 base::FilePath ext_dir
= install_dir
.AppendASCII("extensions")
433 .AppendASCII("Extensions")
434 .AppendASCII("ffffffffffffffffffffffffffffffff");
437 scoped_refptr
<Extension
> extension(extension_file_util::LoadExtension(
438 ext_dir
, Manifest::UNPACKED
, Extension::NO_FLAGS
, &error
));
439 EXPECT_TRUE(extension
.get() == NULL
);
440 EXPECT_STREQ("Could not load extension icon 'icon.png'.", error
.c_str());
442 // Try to install an extension with a zero-length browser action icon file.
443 ext_dir
= install_dir
.AppendASCII("extensions")
445 .AppendASCII("Extensions")
446 .AppendASCII("gggggggggggggggggggggggggggggggg");
448 scoped_refptr
<Extension
> extension2(extension_file_util::LoadExtension(
449 ext_dir
, Manifest::UNPACKED
, Extension::NO_FLAGS
, &error
));
450 EXPECT_TRUE(extension2
.get() == NULL
);
451 EXPECT_STREQ("Could not load icon 'icon.png' for browser action.",
454 // Try to install an extension with a zero-length page action icon file.
455 ext_dir
= install_dir
.AppendASCII("extensions")
457 .AppendASCII("Extensions")
458 .AppendASCII("hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh");
460 scoped_refptr
<Extension
> extension3(extension_file_util::LoadExtension(
461 ext_dir
, Manifest::UNPACKED
, Extension::NO_FLAGS
, &error
));
462 EXPECT_TRUE(extension3
.get() == NULL
);
463 EXPECT_STREQ("Could not load icon 'icon.png' for page action.",
467 // TODO(aa): More tests as motivation allows. Maybe steal some from
468 // ExtensionService? Many of them could probably be tested here without the
469 // MessageLoop shenanigans.