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.
4
5#include "chrome/common/extensions/extension_file_util.h"
6
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"
22
23using extensions::Extension;
24using extensions::Manifest;
25
26namespace keys = extensions::manifest_keys;
27
28class ExtensionFileUtilTest : public testing::Test {
29};
30
31TEST_F(ExtensionFileUtilTest, InstallUninstallGarbageCollect) {
32  base::ScopedTempDir temp;
33  ASSERT_TRUE(temp.CreateUniqueTempDir());
34
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));
40
41  // Create a extensions tree.
42  base::FilePath all_extensions = temp.path().AppendASCII("extensions");
43  ASSERT_TRUE(base::CreateDirectory(all_extensions));
44
45  // Install in empty directory. Should create parent directories as needed.
46  base::FilePath version_1 = extension_file_util::InstallExtension(
47      src,
48      extension_id,
49      version,
50      all_extensions);
51  ASSERT_EQ(version_1.value(),
52            all_extensions.AppendASCII(extension_id).AppendASCII("1.0_0")
53                .value());
54  ASSERT_TRUE(base::DirectoryExists(version_1));
55
56  // Should have moved the source.
57  ASSERT_FALSE(base::DirectoryExists(src));
58
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(
62      src,
63      extension_id,
64      version,
65      all_extensions);
66  ASSERT_EQ(version_2.value(),
67            all_extensions.AppendASCII(extension_id).AppendASCII("1.0_1")
68                .value());
69  ASSERT_TRUE(base::DirectoryExists(version_2));
70
71  // Should have moved the source.
72  ASSERT_FALSE(base::DirectoryExists(src));
73
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(
77      src,
78      extension_id,
79      version,
80      all_extensions);
81  ASSERT_EQ(version_3.value(),
82            all_extensions.AppendASCII(extension_id).AppendASCII("1.0_2")
83                .value());
84  ASSERT_TRUE(base::DirectoryExists(version_3));
85
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,
93                                                extension_paths);
94  ASSERT_FALSE(base::DirectoryExists(version_1));
95  ASSERT_TRUE(base::DirectoryExists(version_2));
96  ASSERT_TRUE(base::DirectoryExists(version_3));
97
98  // Uninstall. Should remove entire extension subtree.
99  extension_file_util::UninstallExtension(all_extensions, extension_id);
100  ASSERT_FALSE(base::DirectoryExists(version_2.DirName()));
101  ASSERT_FALSE(base::DirectoryExists(version_3.DirName()));
102  ASSERT_TRUE(base::DirectoryExists(all_extensions));
103}
104
105TEST_F(ExtensionFileUtilTest, LoadExtensionWithValidLocales) {
106  base::FilePath install_dir;
107  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
108  install_dir = install_dir.AppendASCII("extensions")
109      .AppendASCII("good")
110      .AppendASCII("Extensions")
111      .AppendASCII("behllobkkfkfnphdnhnkndlbkcpglgmj")
112      .AppendASCII("1.0.0.0");
113
114  std::string error;
115  scoped_refptr<Extension> extension(extension_file_util::LoadExtension(
116      install_dir, Manifest::UNPACKED, Extension::NO_FLAGS, &error));
117  ASSERT_TRUE(extension.get() != NULL);
118  EXPECT_EQ("The first extension that I made.", extension->description());
119}
120
121TEST_F(ExtensionFileUtilTest, LoadExtensionWithoutLocalesFolder) {
122  base::FilePath install_dir;
123  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
124  install_dir = install_dir.AppendASCII("extensions")
125      .AppendASCII("good")
126      .AppendASCII("Extensions")
127      .AppendASCII("bjafgdebaacbbbecmhlhpofkepfkgcpa")
128      .AppendASCII("1.0");
129
130  std::string error;
131  scoped_refptr<Extension> extension(extension_file_util::LoadExtension(
132      install_dir, Manifest::UNPACKED, Extension::NO_FLAGS, &error));
133  ASSERT_FALSE(extension.get() == NULL);
134  EXPECT_TRUE(error.empty());
135}
136
137TEST_F(ExtensionFileUtilTest, CheckIllegalFilenamesNoUnderscores) {
138  base::ScopedTempDir temp;
139  ASSERT_TRUE(temp.CreateUniqueTempDir());
140
141  base::FilePath src_path = temp.path().AppendASCII("some_dir");
142  ASSERT_TRUE(base::CreateDirectory(src_path));
143
144  std::string data = "{ \"name\": { \"message\": \"foobar\" } }";
145  ASSERT_TRUE(file_util::WriteFile(src_path.AppendASCII("some_file.txt"),
146                                   data.c_str(), data.length()));
147  std::string error;
148  EXPECT_TRUE(extension_file_util::CheckForIllegalFilenames(temp.path(),
149                                                            &error));
150}
151
152TEST_F(ExtensionFileUtilTest, CheckIllegalFilenamesOnlyReserved) {
153  base::ScopedTempDir temp;
154  ASSERT_TRUE(temp.CreateUniqueTempDir());
155
156  const base::FilePath::CharType* folders[] =
157      { extensions::kLocaleFolder, extensions::kPlatformSpecificFolder };
158
159  for (size_t i = 0; i < arraysize(folders); i++) {
160    base::FilePath src_path = temp.path().Append(folders[i]);
161    ASSERT_TRUE(base::CreateDirectory(src_path));
162  }
163
164  std::string error;
165  EXPECT_TRUE(extension_file_util::CheckForIllegalFilenames(temp.path(),
166                                                            &error));
167}
168
169TEST_F(ExtensionFileUtilTest, CheckIllegalFilenamesReservedAndIllegal) {
170  base::ScopedTempDir temp;
171  ASSERT_TRUE(temp.CreateUniqueTempDir());
172
173  base::FilePath src_path = temp.path().Append(extensions::kLocaleFolder);
174  ASSERT_TRUE(base::CreateDirectory(src_path));
175
176  src_path = temp.path().AppendASCII("_some_dir");
177  ASSERT_TRUE(base::CreateDirectory(src_path));
178
179  std::string error;
180  EXPECT_FALSE(extension_file_util::CheckForIllegalFilenames(temp.path(),
181                                                             &error));
182}
183
184TEST_F(ExtensionFileUtilTest,
185       LoadExtensionGivesHelpfullErrorOnMissingManifest) {
186  base::FilePath install_dir;
187  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
188  install_dir = install_dir.AppendASCII("extensions")
189      .AppendASCII("bad")
190      .AppendASCII("Extensions")
191      .AppendASCII("dddddddddddddddddddddddddddddddd")
192      .AppendASCII("1.0");
193
194  std::string error;
195  scoped_refptr<Extension> extension(extension_file_util::LoadExtension(
196      install_dir, Manifest::UNPACKED, Extension::NO_FLAGS, &error));
197  ASSERT_TRUE(extension.get() == NULL);
198  ASSERT_FALSE(error.empty());
199  ASSERT_STREQ("Manifest file is missing or unreadable.", error.c_str());
200}
201
202TEST_F(ExtensionFileUtilTest, LoadExtensionGivesHelpfullErrorOnBadManifest) {
203  base::FilePath install_dir;
204  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
205  install_dir = install_dir.AppendASCII("extensions")
206      .AppendASCII("bad")
207      .AppendASCII("Extensions")
208      .AppendASCII("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
209      .AppendASCII("1.0");
210
211  std::string error;
212  scoped_refptr<Extension> extension(extension_file_util::LoadExtension(
213      install_dir, Manifest::UNPACKED, Extension::NO_FLAGS, &error));
214  ASSERT_TRUE(extension.get() == NULL);
215  ASSERT_FALSE(error.empty());
216  ASSERT_STREQ("Manifest is not valid JSON.  "
217               "Line: 2, column: 16, Syntax error.", error.c_str());
218}
219
220TEST_F(ExtensionFileUtilTest, FailLoadingNonUTF8Scripts) {
221  base::FilePath install_dir;
222  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
223  install_dir = install_dir.AppendASCII("extensions")
224      .AppendASCII("bad")
225      .AppendASCII("bad_encoding");
226
227  std::string error;
228  scoped_refptr<Extension> extension(extension_file_util::LoadExtension(
229      install_dir, Manifest::UNPACKED, Extension::NO_FLAGS, &error));
230  ASSERT_TRUE(extension.get() == NULL);
231  ASSERT_STREQ("Could not load file 'bad_encoding.js' for content script. "
232               "It isn't UTF-8 encoded.",
233               error.c_str());
234}
235
236static scoped_refptr<Extension> LoadExtensionManifest(
237    base::DictionaryValue* manifest,
238    const base::FilePath& manifest_dir,
239    Manifest::Location location,
240    int extra_flags,
241    std::string* error) {
242  scoped_refptr<Extension> extension = Extension::Create(
243      manifest_dir, location, *manifest, extra_flags, error);
244  return extension;
245}
246
247static scoped_refptr<Extension> LoadExtensionManifest(
248    const std::string& manifest_value,
249    const base::FilePath& manifest_dir,
250    Manifest::Location location,
251    int extra_flags,
252    std::string* error) {
253  JSONStringValueSerializer serializer(manifest_value);
254  scoped_ptr<base::Value> result(serializer.Deserialize(NULL, error));
255  if (!result.get())
256    return NULL;
257  CHECK_EQ(base::Value::TYPE_DICTIONARY, result->GetType());
258  return LoadExtensionManifest(
259      static_cast<base::DictionaryValue*>(result.get()),
260      manifest_dir,
261      location,
262      extra_flags,
263      error);
264}
265
266TEST_F(ExtensionFileUtilTest, ValidateThemeUTF8) {
267  base::ScopedTempDir temp;
268  ASSERT_TRUE(temp.CreateUniqueTempDir());
269
270  // "aeo" with accents. Use http://0xcc.net/jsescape/ to decode them.
271  std::string non_ascii_file = "\xC3\xA0\xC3\xA8\xC3\xB2.png";
272  base::FilePath non_ascii_path =
273      temp.path().Append(base::FilePath::FromUTF8Unsafe(non_ascii_file));
274  file_util::WriteFile(non_ascii_path, "", 0);
275
276  std::string kManifest =
277      base::StringPrintf(
278          "{ \"name\": \"Test\", \"version\": \"1.0\", "
279          "  \"theme\": { \"images\": { \"theme_frame\": \"%s\" } }"
280          "}", non_ascii_file.c_str());
281  std::string error;
282  scoped_refptr<Extension> extension = LoadExtensionManifest(
283      kManifest, temp.path(), Manifest::UNPACKED, 0, &error);
284  ASSERT_TRUE(extension.get()) << error;
285
286  std::vector<extensions::InstallWarning> warnings;
287  EXPECT_TRUE(extension_file_util::ValidateExtension(
288      extension.get(), &error, &warnings)) << error;
289  EXPECT_EQ(0U, warnings.size());
290}
291
292TEST_F(ExtensionFileUtilTest, BackgroundScriptsMustExist) {
293  base::ScopedTempDir temp;
294  ASSERT_TRUE(temp.CreateUniqueTempDir());
295
296  scoped_ptr<base::DictionaryValue> value(new base::DictionaryValue());
297  value->SetString("name", "test");
298  value->SetString("version", "1");
299  value->SetInteger("manifest_version", 1);
300
301  base::ListValue* scripts = new base::ListValue();
302  scripts->Append(new base::StringValue("foo.js"));
303  value->Set("background.scripts", scripts);
304
305  std::string error;
306  std::vector<extensions::InstallWarning> warnings;
307  scoped_refptr<Extension> extension = LoadExtensionManifest(
308      value.get(), temp.path(), Manifest::UNPACKED, 0, &error);
309  ASSERT_TRUE(extension.get()) << error;
310
311  EXPECT_FALSE(extension_file_util::ValidateExtension(
312      extension.get(), &error, &warnings));
313  EXPECT_EQ(l10n_util::GetStringFUTF8(
314      IDS_EXTENSION_LOAD_BACKGROUND_SCRIPT_FAILED, ASCIIToUTF16("foo.js")),
315           error);
316  EXPECT_EQ(0U, warnings.size());
317
318  scripts->Clear();
319  scripts->Append(new base::StringValue("http://google.com/foo.js"));
320
321  extension = LoadExtensionManifest(value.get(), temp.path(),
322                                    Manifest::UNPACKED, 0, &error);
323  ASSERT_TRUE(extension.get()) << error;
324
325  warnings.clear();
326  EXPECT_FALSE(extension_file_util::ValidateExtension(
327      extension.get(), &error, &warnings));
328  EXPECT_EQ(l10n_util::GetStringFUTF8(
329      IDS_EXTENSION_LOAD_BACKGROUND_SCRIPT_FAILED,
330      ASCIIToUTF16("http://google.com/foo.js")),
331           error);
332  EXPECT_EQ(0U, warnings.size());
333}
334
335// Private key, generated by Chrome specifically for this test, and
336// never used elsewhere.
337const char private_key[] =
338    "-----BEGIN PRIVATE KEY-----\n"
339    "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKt02SR0FYaYy6fpW\n"
340    "MAA+kU1BgK3d+OmmWfdr+JATIjhRkyeSF4lTd/71JQsyKqPzYkQPi3EeROWM+goTv\n"
341    "EhJqq07q63BolpsFmlV+S4ny+sBA2B4aWwRYXlBWikdrQSA0mJMzvEHc6nKzBgXik\n"
342    "QSVbyyBNAsxlDB9WaCxRVOpK3AgMBAAECgYBGvSPlrVtAOAQ2V8j9FqorKZA8SLPX\n"
343    "IeJC/yzU3RB2nPMjI17aMOvrUHxJUhzMeh4jwabVvSzzDtKFozPGupW3xaI8sQdi2\n"
344    "WWMTQIk/Q9HHDWoQ9qA6SwX2qWCc5SyjCKqVp78ye+000kqTJYjBsDgXeAlzKcx2B\n"
345    "4GAAeWonDdkQJBANNb8wrqNWFn7DqyQTfELzcRTRnqQ/r1pdeJo6obzbnwGnlqe3t\n"
346    "KhLjtJNIGrQg5iC0OVLWFuvPJs0t3z62A1ckCQQDPq2JZuwTwu5Pl4DJ0r9O1FdqN\n"
347    "JgqPZyMptokCDQ3khLLGakIu+TqB9YtrzI69rJMSG2Egb+6McaDX+dh3XmR/AkB9t\n"
348    "xJf6qDnmA2td/tMtTc0NOk8Qdg/fD8xbZ/YfYMnVoYYs9pQoilBaWRePDRNURMLYZ\n"
349    "vHAI0Llmw7tj7jv17pAkEAz44uXRpjRKtllUIvi5pUENAHwDz+HvdpGH68jpU3hmb\n"
350    "uOwrmnQYxaMReFV68Z2w9DcLZn07f7/R9Wn72z89CxwJAFsDoNaDes4h48bX7plct\n"
351    "s9ACjmTwcCigZjN2K7AGv7ntCLF3DnV5dK0dTHNaAdD3SbY3jl29Rk2CwiURSX6Ee\n"
352    "g==\n"
353    "-----END PRIVATE KEY-----\n";
354
355TEST_F(ExtensionFileUtilTest, FindPrivateKeyFiles) {
356  base::ScopedTempDir temp;
357  ASSERT_TRUE(temp.CreateUniqueTempDir());
358
359  base::FilePath src_path = temp.path().AppendASCII("some_dir");
360  ASSERT_TRUE(base::CreateDirectory(src_path));
361
362  ASSERT_TRUE(file_util::WriteFile(src_path.AppendASCII("a_key.pem"),
363                                   private_key, arraysize(private_key)));
364  ASSERT_TRUE(file_util::WriteFile(src_path.AppendASCII("second_key.pem"),
365                                   private_key, arraysize(private_key)));
366  // Shouldn't find a key with a different extension.
367  ASSERT_TRUE(file_util::WriteFile(src_path.AppendASCII("key.diff_ext"),
368                                   private_key, arraysize(private_key)));
369  // Shouldn't find a key that isn't parsable.
370  ASSERT_TRUE(file_util::WriteFile(src_path.AppendASCII("unparsable_key.pem"),
371                                   private_key, arraysize(private_key) - 30));
372  std::vector<base::FilePath> private_keys =
373      extension_file_util::FindPrivateKeyFiles(temp.path());
374  EXPECT_EQ(2U, private_keys.size());
375  EXPECT_THAT(private_keys,
376              testing::Contains(src_path.AppendASCII("a_key.pem")));
377  EXPECT_THAT(private_keys,
378              testing::Contains(src_path.AppendASCII("second_key.pem")));
379}
380
381TEST_F(ExtensionFileUtilTest, WarnOnPrivateKey) {
382  base::ScopedTempDir temp;
383  ASSERT_TRUE(temp.CreateUniqueTempDir());
384
385  base::FilePath ext_path = temp.path().AppendASCII("ext_root");
386  ASSERT_TRUE(base::CreateDirectory(ext_path));
387
388  const char manifest[] =
389      "{\n"
390      "  \"name\": \"Test Extension\",\n"
391      "  \"version\": \"1.0\",\n"
392      "  \"manifest_version\": 2,\n"
393      "  \"description\": \"The first extension that I made.\"\n"
394      "}\n";
395  ASSERT_TRUE(file_util::WriteFile(ext_path.AppendASCII("manifest.json"),
396                                   manifest, strlen(manifest)));
397  ASSERT_TRUE(file_util::WriteFile(ext_path.AppendASCII("a_key.pem"),
398                                   private_key, strlen(private_key)));
399
400  std::string error;
401  scoped_refptr<Extension> extension(extension_file_util::LoadExtension(
402      ext_path, "the_id", Manifest::EXTERNAL_PREF,
403      Extension::NO_FLAGS, &error));
404  ASSERT_TRUE(extension.get()) << error;
405  ASSERT_EQ(1u, extension->install_warnings().size());
406  EXPECT_THAT(
407      extension->install_warnings(),
408      testing::ElementsAre(
409          testing::Field(
410              &extensions::InstallWarning::message,
411              testing::ContainsRegex(
412                  "extension includes the key file.*ext_root.a_key.pem"))));
413
414  // Turn the warning into an error with ERROR_ON_PRIVATE_KEY.
415  extension = extension_file_util::LoadExtension(
416      ext_path, "the_id", Manifest::EXTERNAL_PREF,
417      Extension::ERROR_ON_PRIVATE_KEY, &error);
418  EXPECT_FALSE(extension.get());
419  EXPECT_THAT(error,
420              testing::ContainsRegex(
421                  "extension includes the key file.*ext_root.a_key.pem"));
422}
423
424TEST_F(ExtensionFileUtilTest, CheckZeroLengthImageFile) {
425  base::FilePath install_dir;
426  ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &install_dir));
427
428  // Try to install an extension with a zero-length icon file.
429  base::FilePath ext_dir = install_dir.AppendASCII("extensions")
430      .AppendASCII("bad")
431      .AppendASCII("Extensions")
432      .AppendASCII("ffffffffffffffffffffffffffffffff");
433
434  std::string error;
435  scoped_refptr<Extension> extension(extension_file_util::LoadExtension(
436      ext_dir, Manifest::UNPACKED, Extension::NO_FLAGS, &error));
437  EXPECT_TRUE(extension.get() == NULL);
438  EXPECT_STREQ("Could not load extension icon 'icon.png'.", error.c_str());
439
440  // Try to install an extension with a zero-length browser action icon file.
441  ext_dir = install_dir.AppendASCII("extensions")
442      .AppendASCII("bad")
443      .AppendASCII("Extensions")
444      .AppendASCII("gggggggggggggggggggggggggggggggg");
445
446  scoped_refptr<Extension> extension2(extension_file_util::LoadExtension(
447      ext_dir, Manifest::UNPACKED, Extension::NO_FLAGS, &error));
448  EXPECT_TRUE(extension2.get() == NULL);
449  EXPECT_STREQ("Could not load icon 'icon.png' for browser action.",
450               error.c_str());
451
452  // Try to install an extension with a zero-length page action icon file.
453  ext_dir = install_dir.AppendASCII("extensions")
454      .AppendASCII("bad")
455      .AppendASCII("Extensions")
456      .AppendASCII("hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh");
457
458  scoped_refptr<Extension> extension3(extension_file_util::LoadExtension(
459      ext_dir, Manifest::UNPACKED, Extension::NO_FLAGS, &error));
460  EXPECT_TRUE(extension3.get() == NULL);
461  EXPECT_STREQ("Could not load icon 'icon.png' for page action.",
462               error.c_str());
463}
464
465// TODO(aa): More tests as motivation allows. Maybe steal some from
466// ExtensionService? Many of them could probably be tested here without the
467// MessageLoop shenanigans.
468