summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcira@chromium.org <cira@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-11-18 18:02:47 +0000
committercira@chromium.org <cira@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-11-18 18:02:47 +0000
commit9428edc493bf5097cc74fca5fcaf4506c4068668 (patch)
treea5db8e196a4e71c71d54ded4c552b2a4d8c50361
parentecc73b2f562a20611804cc07dcff59922dbd09b4 (diff)
downloadchromium_src-9428edc493bf5097cc74fca5fcaf4506c4068668.zip
chromium_src-9428edc493bf5097cc74fca5fcaf4506c4068668.tar.gz
chromium_src-9428edc493bf5097cc74fca5fcaf4506c4068668.tar.bz2
Parse messages.json in ExtensionUnpacker (like we do for manifest) and pass them to sandboxed_extension_unpacker.
Added unittest files for unpacker and sandboxed unpacker. TEST=Try loading any of the unpacker samples added in this CL. They should either pass, or show error and fail. BUG=27362 Review URL: http://codereview.chromium.org/390019 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@32345 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/extensions/extension_file_util.cc72
-rw-r--r--chrome/browser/extensions/sandboxed_extension_unpacker.cc218
-rw-r--r--chrome/browser/extensions/sandboxed_extension_unpacker.h21
-rw-r--r--chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc160
-rw-r--r--chrome/browser/utility_process_host.h11
-rw-r--r--chrome/browser/utility_process_host_unittest.cc3
-rwxr-xr-xchrome/chrome.gyp2
-rw-r--r--chrome/common/extensions/extension.cc8
-rw-r--r--chrome/common/extensions/extension.h5
-rw-r--r--chrome/common/extensions/extension_constants.cc6
-rw-r--r--chrome/common/extensions/extension_constants.h3
-rw-r--r--chrome/common/extensions/extension_unittest.cc11
-rw-r--r--chrome/common/extensions/extension_unpacker.cc74
-rw-r--r--chrome/common/extensions/extension_unpacker.h12
-rw-r--r--chrome/common/extensions/extension_unpacker_unittest.cc120
-rw-r--r--chrome/common/render_messages_internal.h12
-rw-r--r--chrome/test/data/extensions/unpacker/empty_default_locale.crxbin0 -> 521 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/empty_default_locale/manifest.json6
-rw-r--r--chrome/test/data/extensions/unpacker/good_l10n.crxbin0 -> 1185 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/good_l10n/_locales/en_US/messages.json5
-rw-r--r--chrome/test/data/extensions/unpacker/good_l10n/_locales/sr/messages.json5
-rw-r--r--chrome/test/data/extensions/unpacker/good_l10n/manifest.json6
-rw-r--r--chrome/test/data/extensions/unpacker/has_default_missing_locales.crxbin0 -> 539 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/has_default_missing_locales/manifest.json6
-rw-r--r--chrome/test/data/extensions/unpacker/invalid_default_locale.crxbin0 -> 542 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/invalid_default_locale/manifest.json6
-rw-r--r--chrome/test/data/extensions/unpacker/invalid_messages_file.crxbin0 -> 911 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/invalid_messages_file/_locales/en_US/messages.json3
-rw-r--r--chrome/test/data/extensions/unpacker/invalid_messages_file/manifest.json6
-rw-r--r--chrome/test/data/extensions/unpacker/missing_default_data.crxbin0 -> 924 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/missing_default_data/_locales/en_US/messages.json5
-rw-r--r--chrome/test/data/extensions/unpacker/missing_default_data/manifest.json6
-rw-r--r--chrome/test/data/extensions/unpacker/missing_default_has_locales.crxbin0 -> 634 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/missing_default_has_locales/manifest.json5
-rw-r--r--chrome/test/data/extensions/unpacker/missing_messages_file.crxbin0 -> 748 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/missing_messages_file/manifest.json6
-rw-r--r--chrome/test/data/extensions/unpacker/no_l10n.crxbin0 -> 513 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/no_l10n/manifest.json5
-rw-r--r--chrome/test/data/extensions/unpacker/no_locale_data.crxbin0 -> 620 bytes
-rw-r--r--chrome/test/data/extensions/unpacker/no_locale_data/manifest.json6
-rw-r--r--chrome/utility/utility_thread.cc2
41 files changed, 719 insertions, 97 deletions
diff --git a/chrome/browser/extensions/extension_file_util.cc b/chrome/browser/extensions/extension_file_util.cc
index 52c2c78..e7ecd08 100644
--- a/chrome/browser/extensions/extension_file_util.cc
+++ b/chrome/browser/extensions/extension_file_util.cc
@@ -16,8 +16,13 @@
#include "chrome/common/json_value_serializer.h"
#include "net/base/file_stream.h"
+namespace errors = extension_manifest_errors;
+
namespace extension_file_util {
+// Validates locale info. Doesn't check if messages.json files are valid.
+static bool ValidateLocaleInfo(const Extension& extension, std::string* error);
+
const char kInstallDirectoryName[] = "Extensions";
// TODO(mpcomplete): obsolete. remove after migration period.
// http://code.google.com/p/chromium/issues/detail?id=19733
@@ -270,6 +275,10 @@ bool ValidateExtension(Extension* extension, std::string* error) {
}
}
+ // Validate locale info.
+ if (!ValidateLocaleInfo(*extension, error))
+ return false;
+
// Check children of extension root to see if any of them start with _ and is
// not on the reserved list.
if (!CheckForIllegalFilenames(extension->path(), error)) {
@@ -382,6 +391,69 @@ ExtensionMessageBundle* LoadLocaleInfo(const FilePath& extension_path,
return message_bundle;
}
+static bool ValidateLocaleInfo(const Extension& extension, std::string* error) {
+ // default_locale and _locales have to be both present or both missing.
+ const FilePath path = extension.path().AppendASCII(Extension::kLocaleFolder);
+ bool path_exists = file_util::PathExists(path);
+ std::string default_locale = extension.default_locale();
+
+ // If both default locale and _locales folder are empty, skip verification.
+ if (!default_locale.empty() || path_exists) {
+ if (default_locale.empty() && path_exists) {
+ *error = errors::kLocalesNoDefaultLocaleSpecified;
+ return false;
+ } else if (!default_locale.empty() && !path_exists) {
+ *error = errors::kLocalesTreeMissing;
+ return false;
+ }
+
+ // Treat all folders under _locales as valid locales.
+ file_util::FileEnumerator locales(path,
+ false,
+ file_util::FileEnumerator::DIRECTORIES);
+
+ FilePath locale_path = locales.Next();
+ if (locale_path.empty()) {
+ *error = errors::kLocalesTreeMissing;
+ return false;
+ }
+
+ const FilePath default_locale_path = path.AppendASCII(default_locale);
+ bool has_default_locale_message_file = false;
+ do {
+ // Skip any strings with '.'. This happens sometimes, for example with
+ // '.svn' directories.
+ FilePath relative_path;
+ if (!extension.path().AppendRelativePath(locale_path, &relative_path))
+ NOTREACHED();
+ std::wstring subdir(relative_path.ToWStringHack());
+ if (std::find(subdir.begin(), subdir.end(), L'.') != subdir.end())
+ continue;
+
+ FilePath messages_path =
+ locale_path.AppendASCII(Extension::kMessagesFilename);
+
+ if (!file_util::PathExists(messages_path)) {
+ *error = StringPrintf(
+ "%s %s", errors::kLocalesMessagesFileMissing,
+ WideToUTF8(messages_path.ToWStringHack()).c_str());
+ return false;
+ }
+
+ if (locale_path == default_locale_path)
+ has_default_locale_message_file = true;
+ } while (!(locale_path = locales.Next()).empty());
+
+ // Only message file for default locale has to exist.
+ if (!has_default_locale_message_file) {
+ *error = errors::kLocalesNoDefaultMessages;
+ return false;
+ }
+ }
+
+ return true;
+}
+
bool CheckForIllegalFilenames(const FilePath& extension_path,
std::string* error) {
// Reserved underscore names.
diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker.cc b/chrome/browser/extensions/sandboxed_extension_unpacker.cc
index 160a0de..65ccded 100644
--- a/chrome/browser/extensions/sandboxed_extension_unpacker.cc
+++ b/chrome/browser/extensions/sandboxed_extension_unpacker.cc
@@ -21,7 +21,6 @@
#include "chrome/common/extensions/extension_unpacker.h"
#include "chrome/common/json_value_serializer.h"
#include "net/base/base64.h"
-
#include "third_party/skia/include/core/SkBitmap.h"
const char SandboxedExtensionUnpacker::kExtensionHeaderMagic[] = "Cr24";
@@ -83,7 +82,8 @@ void SandboxedExtensionUnpacker::Start() {
// Otherwise, unpack the extension in this process.
ExtensionUnpacker unpacker(temp_crx_path);
if (unpacker.Run() && unpacker.DumpImagesToFile())
- OnUnpackExtensionSucceeded(*unpacker.parsed_manifest());
+ OnUnpackExtensionSucceeded(*unpacker.parsed_manifest(),
+ *unpacker.parsed_catalogs());
else
OnUnpackExtensionFailed(unpacker.error_message());
}
@@ -97,38 +97,16 @@ void SandboxedExtensionUnpacker::StartProcessOnIOThread(
}
void SandboxedExtensionUnpacker::OnUnpackExtensionSucceeded(
- const DictionaryValue& manifest) {
- DCHECK(ChromeThread::CurrentlyOn(thread_identifier_));
+ const DictionaryValue& manifest,
+ const DictionaryValue& catalogs) {
+ // Skip check for unittests.
+ if (thread_identifier_ != ChromeThread::ID_COUNT)
+ DCHECK(ChromeThread::CurrentlyOn(thread_identifier_));
got_response_ = true;
- ExtensionUnpacker::DecodedImages images;
- if (!ExtensionUnpacker::ReadImagesFromFile(temp_dir_.path(), &images)) {
- ReportFailure("Couldn't read image data from disk.");
+ scoped_ptr<DictionaryValue> final_manifest(RewriteManifestFile(manifest));
+ if (!final_manifest.get())
return;
- }
-
- // Add the public key extracted earlier to the parsed manifest and overwrite
- // the original manifest. We do this to ensure the manifest doesn't contain an
- // exploitable bug that could be used to compromise the browser.
- scoped_ptr<DictionaryValue> final_manifest(
- static_cast<DictionaryValue*>(manifest.DeepCopy()));
- final_manifest->SetString(extension_manifest_keys::kPublicKey, public_key_);
-
- std::string manifest_json;
- JSONStringValueSerializer serializer(&manifest_json);
- serializer.set_pretty_print(true);
- if (!serializer.Serialize(*final_manifest)) {
- ReportFailure("Error serializing manifest.json.");
- return;
- }
-
- FilePath manifest_path =
- extension_root_.AppendASCII(Extension::kManifestFilename);
- if (!file_util::WriteFile(manifest_path,
- manifest_json.data(), manifest_json.size())) {
- ReportFailure("Error saving manifest.json.");
- return;
- }
// Create an extension object that refers to the temporary location the
// extension was unpacked to. We use this until the extension is finally
@@ -144,55 +122,11 @@ void SandboxedExtensionUnpacker::OnUnpackExtensionSucceeded(
return;
}
- // Delete any images that may be used by the browser. We're going to write
- // out our own versions of the parsed images, and we want to make sure the
- // originals are gone for good.
- std::set<FilePath> image_paths = extension_->GetBrowserImages();
- if (image_paths.size() != images.size()) {
- ReportFailure("Decoded images don't match what's in the manifest.");
+ if (!RewriteImageFiles())
return;
- }
-
- for (std::set<FilePath>::iterator it = image_paths.begin();
- it != image_paths.end(); ++it) {
- FilePath path = *it;
- if (path.IsAbsolute() || path.ReferencesParent()) {
- ReportFailure("Invalid path for browser image.");
- return;
- }
- if (!file_util::Delete(extension_root_.Append(path), false)) {
- ReportFailure("Error removing old image file.");
- return;
- }
- }
- // Write our parsed images back to disk as well.
- for (size_t i = 0; i < images.size(); ++i) {
- const SkBitmap& image = images[i].a;
- FilePath path_suffix = images[i].b;
- if (path_suffix.IsAbsolute() || path_suffix.ReferencesParent()) {
- ReportFailure("Invalid path for bitmap image.");
- return;
- }
- FilePath path = extension_root_.Append(path_suffix);
-
- std::vector<unsigned char> image_data;
- // TODO(mpcomplete): It's lame that we're encoding all images as PNG, even
- // though they may originally be .jpg, etc. Figure something out.
- // http://code.google.com/p/chromium/issues/detail?id=12459
- if (!gfx::PNGCodec::EncodeBGRASkBitmap(image, false, &image_data)) {
- ReportFailure("Error re-encoding theme image.");
- return;
- }
-
- // Note: we're overwriting existing files that the utility process wrote,
- // so we can be sure the directory exists.
- const char* image_data_ptr = reinterpret_cast<const char*>(&image_data[0]);
- if (!file_util::WriteFile(path, image_data_ptr, image_data.size())) {
- ReportFailure("Error saving theme image.");
- return;
- }
- }
+ if (!RewriteCatalogFiles(catalogs))
+ return;
ReportSuccess();
}
@@ -310,3 +244,131 @@ void SandboxedExtensionUnpacker::ReportSuccess() {
client_->OnUnpackSuccess(temp_dir_.Take(), extension_root_,
extension_.release());
}
+
+DictionaryValue* SandboxedExtensionUnpacker::RewriteManifestFile(
+ const DictionaryValue& manifest) {
+ // Add the public key extracted earlier to the parsed manifest and overwrite
+ // the original manifest. We do this to ensure the manifest doesn't contain an
+ // exploitable bug that could be used to compromise the browser.
+ scoped_ptr<DictionaryValue> final_manifest(
+ static_cast<DictionaryValue*>(manifest.DeepCopy()));
+ final_manifest->SetString(extension_manifest_keys::kPublicKey, public_key_);
+
+ std::string manifest_json;
+ JSONStringValueSerializer serializer(&manifest_json);
+ serializer.set_pretty_print(true);
+ if (!serializer.Serialize(*final_manifest)) {
+ ReportFailure("Error serializing manifest.json.");
+ return NULL;
+ }
+
+ FilePath manifest_path =
+ extension_root_.AppendASCII(Extension::kManifestFilename);
+ if (!file_util::WriteFile(manifest_path,
+ manifest_json.data(), manifest_json.size())) {
+ ReportFailure("Error saving manifest.json.");
+ return NULL;
+ }
+
+ return final_manifest.release();
+}
+
+bool SandboxedExtensionUnpacker::RewriteImageFiles() {
+ ExtensionUnpacker::DecodedImages images;
+ if (!ExtensionUnpacker::ReadImagesFromFile(temp_dir_.path(), &images)) {
+ ReportFailure("Couldn't read image data from disk.");
+ return false;
+ }
+
+ // Delete any images that may be used by the browser. We're going to write
+ // out our own versions of the parsed images, and we want to make sure the
+ // originals are gone for good.
+ std::set<FilePath> image_paths = extension_->GetBrowserImages();
+ if (image_paths.size() != images.size()) {
+ ReportFailure("Decoded images don't match what's in the manifest.");
+ return false;
+ }
+
+ for (std::set<FilePath>::iterator it = image_paths.begin();
+ it != image_paths.end(); ++it) {
+ FilePath path = *it;
+ if (path.IsAbsolute() || path.ReferencesParent()) {
+ ReportFailure("Invalid path for browser image.");
+ return false;
+ }
+ if (!file_util::Delete(extension_root_.Append(path), false)) {
+ ReportFailure("Error removing old image file.");
+ return false;
+ }
+ }
+
+ // Write our parsed images back to disk as well.
+ for (size_t i = 0; i < images.size(); ++i) {
+ const SkBitmap& image = images[i].a;
+ FilePath path_suffix = images[i].b;
+ if (path_suffix.IsAbsolute() || path_suffix.ReferencesParent()) {
+ ReportFailure("Invalid path for bitmap image.");
+ return false;
+ }
+ FilePath path = extension_root_.Append(path_suffix);
+
+ std::vector<unsigned char> image_data;
+ // TODO(mpcomplete): It's lame that we're encoding all images as PNG, even
+ // though they may originally be .jpg, etc. Figure something out.
+ // http://code.google.com/p/chromium/issues/detail?id=12459
+ if (!gfx::PNGCodec::EncodeBGRASkBitmap(image, false, &image_data)) {
+ ReportFailure("Error re-encoding theme image.");
+ return false;
+ }
+
+ // Note: we're overwriting existing files that the utility process wrote,
+ // so we can be sure the directory exists.
+ const char* image_data_ptr = reinterpret_cast<const char*>(&image_data[0]);
+ if (!file_util::WriteFile(path, image_data_ptr, image_data.size())) {
+ ReportFailure("Error saving theme image.");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool SandboxedExtensionUnpacker::RewriteCatalogFiles(
+ const DictionaryValue& catalogs) {
+ // Write our parsed catalogs back to disk.
+ DictionaryValue::key_iterator key_it = catalogs.begin_keys();
+ for (; key_it != catalogs.end_keys(); ++key_it) {
+ DictionaryValue* catalog;
+ if (!catalogs.GetDictionary(*key_it, &catalog)) {
+ ReportFailure("Invalid catalog data.");
+ return false;
+ }
+
+ FilePath relative_path = FilePath::FromWStringHack(*key_it);
+ relative_path = relative_path.AppendASCII(Extension::kMessagesFilename);
+ if (relative_path.IsAbsolute() || relative_path.ReferencesParent()) {
+ ReportFailure("Invalid path for catalog.");
+ return false;
+ }
+ FilePath path = extension_root_.Append(relative_path);
+
+ std::string catalog_json;
+ JSONStringValueSerializer serializer(&catalog_json);
+ serializer.set_pretty_print(true);
+ if (!serializer.Serialize(*catalog)) {
+ ReportFailure("Error serializing catalog.");
+ return false;
+ }
+
+ // Note: we're overwriting existing files that the utility process read,
+ // so we can be sure the directory exists.
+ if (!file_util::WriteFile(path,
+ catalog_json.c_str(),
+ catalog_json.size())) {
+ ReportFailure("Error saving catalog.");
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker.h b/chrome/browser/extensions/sandboxed_extension_unpacker.h
index 06edcbe..9c04c9f 100644
--- a/chrome/browser/extensions/sandboxed_extension_unpacker.h
+++ b/chrome/browser/extensions/sandboxed_extension_unpacker.h
@@ -20,7 +20,7 @@ class ResourceDispatcherHost;
class SandboxedExtensionUnpackerClient
: public base::RefCountedThreadSafe<SandboxedExtensionUnpackerClient> {
public:
- // temp_dir - A temporary directoy containing the results of the extension
+ // temp_dir - A temporary directory containing the results of the extension
// unpacking. The client is responsible for deleting this directory.
//
// extension_root - The path to the extension root inside of temp_dir.
@@ -44,9 +44,9 @@ class SandboxedExtensionUnpackerClient
// sources.
//
// Unpacking an extension using this class makes minor changes to its source,
-// such as transcoding all images to PNG and rewriting the manifest JSON. As
-// such, it should not be used when the output is not intended to be given back
-// to the author.
+// such as transcoding all images to PNG, parsing all message catalogs
+// and rewriting the manifest JSON. As such, it should not be used when the
+// output is not intended to be given back to the author.
//
//
// Lifetime management:
@@ -102,6 +102,7 @@ class SandboxedExtensionUnpacker : public UtilityProcessHost::Client {
private:
class ProcessHostClient;
friend class ProcessHostClient;
+ friend class SandboxedExtensionUnpackerTest;
~SandboxedExtensionUnpacker() {}
@@ -120,13 +121,23 @@ class SandboxedExtensionUnpacker : public UtilityProcessHost::Client {
void StartProcessOnIOThread(const FilePath& temp_crx_path);
// SandboxedExtensionUnpacker
- void OnUnpackExtensionSucceeded(const DictionaryValue& manifest);
+ void OnUnpackExtensionSucceeded(const DictionaryValue& manifest,
+ const DictionaryValue& catalogs);
void OnUnpackExtensionFailed(const std::string& error_message);
void OnProcessCrashed();
void ReportFailure(const std::string& message);
void ReportSuccess();
+ // Overwrites original manifest with safe result from utility process.
+ // Returns NULL on error. Caller owns the returned object.
+ DictionaryValue* RewriteManifestFile(const DictionaryValue& manifest);
+
+ // Overwrites original files with safe results from utility process.
+ // Reports error and returns false if it fails.
+ bool RewriteImageFiles();
+ bool RewriteCatalogFiles(const DictionaryValue& parsed_catalogs);
+
FilePath crx_path_;
ChromeThread::ID thread_identifier_;
ResourceDispatcherHost* rdh_;
diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc b/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc
new file mode 100644
index 0000000..93f550d
--- /dev/null
+++ b/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc
@@ -0,0 +1,160 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/ref_counted.h"
+#include "base/scoped_temp_dir.h"
+#include "base/string_util.h"
+#include "base/values.h"
+#include "chrome/browser/extensions/sandboxed_extension_unpacker.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/extensions/extension.h"
+#include "chrome/common/extensions/extension_constants.h"
+#include "chrome/common/extensions/extension_unpacker.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace errors = extension_manifest_errors;
+namespace keys = extension_manifest_keys;
+
+using testing::_;
+using testing::Invoke;
+
+void OnUnpackSuccess(const FilePath& temp_dir,
+ const FilePath& extension_root,
+ Extension* extension) {
+ delete extension;
+ // Don't delete temp_dir here, we need to do some post op checking.
+}
+
+class MockSandboxedExtensionUnpackerClient
+ : public SandboxedExtensionUnpackerClient {
+ public:
+ virtual ~MockSandboxedExtensionUnpackerClient() {}
+
+ MOCK_METHOD3(OnUnpackSuccess,
+ void(const FilePath& temp_dir,
+ const FilePath& extension_root,
+ Extension* extension));
+
+ MOCK_METHOD1(OnUnpackFailure,
+ void(const std::string& error));
+
+ void DelegateToFake() {
+ ON_CALL(*this, OnUnpackSuccess(_, _, _))
+ .WillByDefault(Invoke(::OnUnpackSuccess));
+ }
+};
+
+class SandboxedExtensionUnpackerTest : public testing::Test {
+ public:
+ virtual void SetUp () {
+ // It will delete itself.
+ client_ = new MockSandboxedExtensionUnpackerClient;
+ client_->DelegateToFake();
+ }
+
+ virtual void TearDown() {
+ // Clean up finally.
+ ASSERT_TRUE(file_util::Delete(install_dir_, true)) <<
+ install_dir_.value();
+ }
+
+ void SetupUnpacker(const std::string& crx_name) {
+ FilePath original_path;
+ ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &original_path));
+ original_path = original_path.AppendASCII("extensions")
+ .AppendASCII("unpacker")
+ .AppendASCII(crx_name);
+ ASSERT_TRUE(file_util::PathExists(original_path)) << original_path.value();
+
+ // Try bots won't let us write into DIR_TEST_DATA, so we have to create
+ // a temp folder to play in.
+ ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &install_dir_));
+ install_dir_ =
+ install_dir_.AppendASCII("sandboxed_extension_unpacker_test");
+ ASSERT_TRUE(file_util::Delete(install_dir_, true)) <<
+ install_dir_.value();
+ ASSERT_TRUE(file_util::CreateDirectory(install_dir_)) <<
+ install_dir_.value();
+
+ FilePath crx_path = install_dir_.AppendASCII(crx_name);
+ ASSERT_TRUE(file_util::CopyFile(original_path, crx_path)) <<
+ "Original path: " << original_path.value() <<
+ ", Crx path: " << crx_path.value();
+
+ unpacker_.reset(new ExtensionUnpacker(crx_path));
+
+ // It will delete itself.
+ sandboxed_unpacker_ =
+ new SandboxedExtensionUnpacker(crx_path, NULL, client_);
+ PrepareUnpackerEnv();
+ }
+
+ void PrepareUnpackerEnv() {
+ sandboxed_unpacker_->extension_root_ =
+ install_dir_.AppendASCII("TEMP_INSTALL");
+
+ sandboxed_unpacker_->temp_dir_.Set(install_dir_);
+ sandboxed_unpacker_->public_key_ =
+ "ocnapchkplbmjmpfehjocmjnipfmogkh";
+ }
+
+ void OnUnpackSucceeded() {
+ sandboxed_unpacker_->OnUnpackExtensionSucceeded(
+ *unpacker_->parsed_manifest(),
+ *unpacker_->parsed_catalogs());
+ }
+
+ FilePath GetInstallPath() {
+ return install_dir_.AppendASCII("TEMP_INSTALL");
+ }
+
+ protected:
+ FilePath install_dir_;
+ MockSandboxedExtensionUnpackerClient* client_;
+ scoped_ptr<ExtensionUnpacker> unpacker_;
+ scoped_refptr<SandboxedExtensionUnpacker> sandboxed_unpacker_;
+};
+
+TEST_F(SandboxedExtensionUnpackerTest, NoCatalogsSuccess) {
+ EXPECT_CALL(*client_, OnUnpackSuccess(_, _, _));
+
+ SetupUnpacker("no_l10n.crx");
+ ASSERT_TRUE(unpacker_->Run());
+ ASSERT_TRUE(unpacker_->DumpImagesToFile());
+
+ // Check that there is no _locales folder.
+ FilePath install_path =
+ GetInstallPath().AppendASCII(Extension::kLocaleFolder);
+ EXPECT_FALSE(file_util::PathExists(install_path));
+
+ OnUnpackSucceeded();
+
+ // Check that there still is no _locales folder.
+ EXPECT_FALSE(file_util::PathExists(install_path));
+}
+
+TEST_F(SandboxedExtensionUnpackerTest, WithCatalogsSuccess) {
+ EXPECT_CALL(*client_, OnUnpackSuccess(_, _, _));
+
+ SetupUnpacker("good_l10n.crx");
+ ASSERT_TRUE(unpacker_->Run());
+ ASSERT_TRUE(unpacker_->DumpImagesToFile());
+
+ // Delete _locales/en_US/messages.json.
+ FilePath messages_file;
+ messages_file = GetInstallPath()
+ .AppendASCII(Extension::kLocaleFolder)
+ .AppendASCII("en_US")
+ .AppendASCII(Extension::kMessagesFilename);
+ EXPECT_TRUE(file_util::Delete(messages_file, false));
+
+ OnUnpackSucceeded();
+
+ // Check that there is _locales/en_US/messages.json file.
+ EXPECT_TRUE(file_util::PathExists(messages_file));
+}
diff --git a/chrome/browser/utility_process_host.h b/chrome/browser/utility_process_host.h
index a37a968..773b515 100644
--- a/chrome/browser/utility_process_host.h
+++ b/chrome/browser/utility_process_host.h
@@ -35,9 +35,11 @@ class UtilityProcessHost : public ChildProcessHost {
virtual void OnProcessCrashed() {}
// Called when the extension has unpacked successfully. |manifest| is the
- // parsed manifest.json file. |images| contains a list of decoded images
- // and the associated paths where those images live on disk.
- virtual void OnUnpackExtensionSucceeded(const DictionaryValue& manifest) {}
+ // parsed manifest.json file. |catalogs| contains list of all parsed
+ // message catalogs. |images| contains a list of decoded images and the
+ // associated paths where those images live on disk.
+ virtual void OnUnpackExtensionSucceeded(const DictionaryValue& manifest,
+ const DictionaryValue& catalogs) {}
// Called when an error occurred while unpacking the extension.
// |error_message| contains a description of the problem.
@@ -51,8 +53,7 @@ class UtilityProcessHost : public ChildProcessHost {
// Called when an error occurred while parsing the resource data.
// |error_message| contains a description of the problem.
- virtual void OnUnpackWebResourceFailed(
- const std::string& error_message) {}
+ virtual void OnUnpackWebResourceFailed(const std::string& error_message) {}
// Called when an update manifest xml file was successfully parsed.
virtual void OnParseUpdateManifestSucceeded(
diff --git a/chrome/browser/utility_process_host_unittest.cc b/chrome/browser/utility_process_host_unittest.cc
index 950c596..44fb0f8 100644
--- a/chrome/browser/utility_process_host_unittest.cc
+++ b/chrome/browser/utility_process_host_unittest.cc
@@ -43,7 +43,8 @@ class TestUtilityProcessHostClient : public UtilityProcessHost::Client {
NOTREACHED();
}
- virtual void OnUnpackExtensionSucceeded(const DictionaryValue& manifest) {
+ virtual void OnUnpackExtensionSucceeded(const DictionaryValue& manifest,
+ const DictionaryValue& catalogs) {
success_ = true;
}
diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp
index 969c303..98c4a90 100755
--- a/chrome/chrome.gyp
+++ b/chrome/chrome.gyp
@@ -4665,6 +4665,7 @@
'browser/extensions/extension_updater_unittest.cc',
'browser/extensions/extensions_service_unittest.cc',
'browser/extensions/file_reader_unittest.cc',
+ 'browser/extensions/sandboxed_extension_unpacker_unittest.cc',
'browser/extensions/user_script_listener_unittest.cc',
'browser/extensions/user_script_master_unittest.cc',
'browser/find_backend_unittest.cc',
@@ -4788,6 +4789,7 @@
'common/extensions/extension_action_unittest.cc',
'common/extensions/extension_l10n_util_unittest.cc',
'common/extensions/extension_message_bundle_unittest.cc',
+ 'common/extensions/extension_unpacker_unittest.cc',
'common/extensions/update_manifest_unittest.cc',
'common/extensions/url_pattern_unittest.cc',
'common/extensions/user_script_unittest.cc',
diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc
index f09fba8..d07ba80 100644
--- a/chrome/common/extensions/extension.cc
+++ b/chrome/common/extensions/extension.cc
@@ -1113,6 +1113,14 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_id,
}
}
+ if (source.HasKey(keys::kDefaultLocale)) {
+ if (!source.GetString(keys::kDefaultLocale, &default_locale_) ||
+ default_locale_.empty()) {
+ *error = errors::kInvalidDefaultLocale;
+ return false;
+ }
+ }
+
// Chrome URL overrides (optional)
if (source.HasKey(keys::kChromeURLOverrides)) {
DictionaryValue* overrides;
diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h
index 8cd5da3..7738acf 100644
--- a/chrome/common/extensions/extension.h
+++ b/chrome/common/extensions/extension.h
@@ -274,6 +274,8 @@ class Extension {
message_bundle_.reset(message_bundle);
}
+ const std::string default_locale() const { return default_locale_; }
+
// Chrome URL overrides (see ExtensionOverrideUI).
const URLOverrideMap& GetChromeURLOverrides() const {
return chrome_url_overrides_;
@@ -406,6 +408,9 @@ class Extension {
// Handles the l10n messages replacement and parsing.
scoped_ptr<ExtensionMessageBundle> message_bundle_;
+ // Default locale for fall back. Can be empty if extension is not localized.
+ std::string default_locale_;
+
// A map of chrome:// hostnames (newtab, downloads, etc.) to Extension URLs
// which override the handling of those URLs.
URLOverrideMap chrome_url_overrides_;
diff --git a/chrome/common/extensions/extension_constants.cc b/chrome/common/extensions/extension_constants.cc
index c2700dc..fc16fea 100644
--- a/chrome/common/extensions/extension_constants.cc
+++ b/chrome/common/extensions/extension_constants.cc
@@ -186,8 +186,14 @@ const char* kThemesCannotContainExtensions =
"A theme cannot contain extensions code.";
const char* kLocalesNoDefaultLocaleSpecified =
"Localization used, but default_locale wasn't specified in the manifest.";
+const char* kLocalesNoDefaultMessages =
+ "Default locale is defined but default data couldn't be loaded.";
const char* kLocalesNoValidLocaleNamesListed =
"No valid locale name could be found in _locales directory.";
+const char* kLocalesTreeMissing =
+ "Default locale was specified, but _locales subtree is missing.";
+const char* kLocalesMessagesFileMissing =
+ "Messages file is missing for locale.";
const char* kInvalidOptionsPage =
"Invalid value for 'options_page'.";
} // namespace extension_manifest_errors
diff --git a/chrome/common/extensions/extension_constants.h b/chrome/common/extensions/extension_constants.h
index ff9836f..b0765b4 100644
--- a/chrome/common/extensions/extension_constants.h
+++ b/chrome/common/extensions/extension_constants.h
@@ -124,7 +124,10 @@ namespace extension_manifest_errors {
extern const char* kInvalidUpdateURL;
extern const char* kInvalidDefaultLocale;
extern const char* kLocalesNoDefaultLocaleSpecified;
+ extern const char* kLocalesNoDefaultMessages;
extern const char* kLocalesNoValidLocaleNamesListed;
+ extern const char* kLocalesTreeMissing;
+ extern const char* kLocalesMessagesFileMissing;
extern const char* kInvalidOptionsPage;
} // namespace extension_manifest_errors
diff --git a/chrome/common/extensions/extension_unittest.cc b/chrome/common/extensions/extension_unittest.cc
index 6e1ad5e..db54776 100644
--- a/chrome/common/extensions/extension_unittest.cc
+++ b/chrome/common/extensions/extension_unittest.cc
@@ -49,6 +49,7 @@ TEST(ExtensionTest, DISABLED_InitFromValueInvalid) {
ASSERT_TRUE(valid_value.get());
ASSERT_TRUE(extension.InitFromValue(*valid_value, true, &error));
ASSERT_EQ("", error);
+ EXPECT_EQ("en_US", extension.default_locale());
scoped_ptr<DictionaryValue> input_value;
@@ -238,6 +239,16 @@ TEST(ExtensionTest, DISABLED_InitFromValueInvalid) {
input_value->Set(keys::kOptionsPage, Value::CreateNullValue());
EXPECT_FALSE(extension.InitFromValue(*input_value, true, &error));
EXPECT_TRUE(MatchPattern(error, errors::kInvalidOptionsPage));
+
+ // Test invalid/empty default locale.
+ input_value.reset(static_cast<DictionaryValue*>(valid_value->DeepCopy()));
+ input_value->Set(keys::kDefaultLocale, Value::CreateIntegerValue(5));
+ EXPECT_FALSE(extension.InitFromValue(*input_value, true, &error));
+ EXPECT_TRUE(MatchPattern(error, errors::kInvalidDefaultLocale));
+
+ input_value->Set(keys::kDefaultLocale, Value::CreateStringValue(""));
+ EXPECT_FALSE(extension.InitFromValue(*input_value, true, &error));
+ EXPECT_TRUE(MatchPattern(error, errors::kInvalidDefaultLocale));
}
TEST(ExtensionTest, InitFromValueValid) {
diff --git a/chrome/common/extensions/extension_unpacker.cc b/chrome/common/extensions/extension_unpacker.cc
index 95c7e0d..e5c7758 100644
--- a/chrome/common/extensions/extension_unpacker.cc
+++ b/chrome/common/extensions/extension_unpacker.cc
@@ -23,6 +23,9 @@
#include "third_party/skia/include/core/SkBitmap.h"
#include "webkit/glue/image_decoder.h"
+namespace errors = extension_manifest_errors;
+namespace keys = extension_manifest_keys;
+
namespace {
// The name of a temporary directory to install an extension into for
// validation before finalizing install.
@@ -87,7 +90,7 @@ DictionaryValue* ExtensionUnpacker::ReadManifest() {
FilePath manifest_path =
temp_install_dir_.AppendASCII(Extension::kManifestFilename);
if (!file_util::PathExists(manifest_path)) {
- SetError(extension_manifest_errors::kInvalidManifest);
+ SetError(errors::kInvalidManifest);
return NULL;
}
@@ -100,13 +103,46 @@ DictionaryValue* ExtensionUnpacker::ReadManifest() {
}
if (!root->IsType(Value::TYPE_DICTIONARY)) {
- SetError(extension_manifest_errors::kInvalidManifest);
+ SetError(errors::kInvalidManifest);
return NULL;
}
return static_cast<DictionaryValue*>(root.release());
}
+bool ExtensionUnpacker::ReadAllMessageCatalogs(
+ const std::string& default_locale) {
+ FilePath locales_path =
+ temp_install_dir_.AppendASCII(Extension::kLocaleFolder);
+
+ // Treat all folders under _locales as valid locales.
+ file_util::FileEnumerator locales(locales_path,
+ false,
+ file_util::FileEnumerator::DIRECTORIES);
+
+ FilePath locale_path = locales.Next();
+ do {
+ // Since we use this string as a key in a DictionaryValue, be paranoid about
+ // skipping any strings with '.'. This happens sometimes, for example with
+ // '.svn' directories.
+ FilePath relative_path;
+ // message_path was created from temp_install_dir. This should never fail.
+ if (!temp_install_dir_.AppendRelativePath(locale_path, &relative_path))
+ NOTREACHED();
+ std::wstring subdir(relative_path.ToWStringHack());
+ if (std::find(subdir.begin(), subdir.end(), L'.') != subdir.end())
+ continue;
+
+ FilePath messages_path =
+ locale_path.AppendASCII(Extension::kMessagesFilename);
+
+ if (!ReadMessageCatalog(messages_path))
+ return false;
+ } while (!(locale_path = locales.Next()).empty());
+
+ return true;
+}
+
bool ExtensionUnpacker::Run() {
LOG(INFO) << "Installing extension " << extension_path_.value();
@@ -154,6 +190,13 @@ bool ExtensionUnpacker::Run() {
return false; // Error was already reported.
}
+ // Parse all message catalogs (if any).
+ parsed_catalogs_.reset(new DictionaryValue);
+ if (!extension.default_locale().empty()) {
+ if (!ReadAllMessageCatalogs(extension.default_locale()))
+ return false; // Error was already reported.
+ }
+
return true;
}
@@ -201,6 +244,33 @@ bool ExtensionUnpacker::AddDecodedImage(const FilePath& path) {
return true;
}
+bool ExtensionUnpacker::ReadMessageCatalog(const FilePath& message_path) {
+ std::string error;
+ JSONFileValueSerializer serializer(message_path);
+ DictionaryValue* root =
+ static_cast<DictionaryValue*>(serializer.Deserialize(&error));
+ if (!root) {
+ std::string messages_file = WideToASCII(message_path.ToWStringHack());
+ if (error.empty()) {
+ // If file is missing, Deserialize will fail with empty error.
+ SetError(StringPrintf("%s %s", errors::kLocalesMessagesFileMissing,
+ messages_file.c_str()));
+ } else {
+ SetError(StringPrintf("%s: %s", messages_file.c_str(), error.c_str()));
+ }
+ return false;
+ }
+
+ FilePath relative_path;
+ // message_path was created from temp_install_dir. This should never fail.
+ if (!temp_install_dir_.AppendRelativePath(message_path, &relative_path))
+ NOTREACHED();
+
+ parsed_catalogs_->Set(relative_path.DirName().ToWStringHack(), root);
+
+ return true;
+}
+
void ExtensionUnpacker::SetError(const std::string &error) {
error_message_ = error;
}
diff --git a/chrome/common/extensions/extension_unpacker.h b/chrome/common/extensions/extension_unpacker.h
index aeb72d9..c59262d 100644
--- a/chrome/common/extensions/extension_unpacker.h
+++ b/chrome/common/extensions/extension_unpacker.h
@@ -46,16 +46,24 @@ class ExtensionUnpacker {
return parsed_manifest_.get();
}
const DecodedImages& decoded_images() { return decoded_images_; }
+ DictionaryValue* parsed_catalogs() { return parsed_catalogs_.get(); }
private:
// Parse the manifest.json file inside the extension (not in the header).
// Caller takes ownership of return value.
DictionaryValue* ReadManifest();
+ // Parse all _locales/*/messages.json files inside the extension.
+ bool ReadAllMessageCatalogs(const std::string& default_locale);
+
// Decodes the image at the given path and puts it in our list of decoded
// images.
bool AddDecodedImage(const FilePath& path);
+ // Parses the catalog at the given path and puts it in our list of parsed
+ // catalogs.
+ bool ReadMessageCatalog(const FilePath& message_path);
+
// Set the error message.
void SetError(const std::string& error);
@@ -72,6 +80,10 @@ class ExtensionUnpacker {
// are relative to the manifest file.
DecodedImages decoded_images_;
+ // Dictionary of relative paths and catalogs per path. Paths are in the form
+ // of _locales/locale, without messages.json base part.
+ scoped_ptr<DictionaryValue> parsed_catalogs_;
+
// The last error message that was set. Empty if there were no errors.
std::string error_message_;
diff --git a/chrome/common/extensions/extension_unpacker_unittest.cc b/chrome/common/extensions/extension_unpacker_unittest.cc
new file mode 100644
index 0000000..f7679cf
--- /dev/null
+++ b/chrome/common/extensions/extension_unpacker_unittest.cc
@@ -0,0 +1,120 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/string_util.h"
+#include "base/values.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/extensions/extension_constants.h"
+#include "chrome/common/extensions/extension_unpacker.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/skia/include/core/SkBitmap.h"
+
+namespace errors = extension_manifest_errors;
+namespace keys = extension_manifest_keys;
+
+class ExtensionUnpackerTest : public testing::Test {
+public:
+ void SetupUnpacker(const std::string& crx_name) {
+ FilePath original_path;
+ ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &original_path));
+ original_path = original_path.AppendASCII("extensions")
+ .AppendASCII("unpacker")
+ .AppendASCII(crx_name);
+ ASSERT_TRUE(file_util::PathExists(original_path)) << original_path.value();
+
+ // Try bots won't let us write into DIR_TEST_DATA, so we have to create
+ // a temp folder to play in.
+ ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &install_dir_));
+ install_dir_ = install_dir_.AppendASCII("extension_unpacker_test");
+ ASSERT_TRUE(file_util::Delete(install_dir_, true)) <<
+ install_dir_.value();
+ ASSERT_TRUE(file_util::CreateDirectory(install_dir_)) <<
+ install_dir_.value();
+
+ FilePath crx_path = install_dir_.AppendASCII(crx_name);
+ ASSERT_TRUE(file_util::CopyFile(original_path, crx_path)) <<
+ "Original path " << original_path.value() <<
+ ", Crx path " << crx_path.value();
+
+ unpacker_.reset(new ExtensionUnpacker(crx_path));
+ }
+
+ virtual void TearDown() {
+ ASSERT_TRUE(file_util::Delete(install_dir_, true)) <<
+ install_dir_.value();
+ }
+
+ protected:
+ FilePath install_dir_;
+ scoped_ptr<ExtensionUnpacker> unpacker_;
+};
+
+TEST_F(ExtensionUnpackerTest, EmptyDefaultLocale) {
+ SetupUnpacker("empty_default_locale.crx");
+ EXPECT_FALSE(unpacker_->Run());
+ EXPECT_EQ(errors::kInvalidDefaultLocale, unpacker_->error_message());
+}
+
+TEST_F(ExtensionUnpackerTest, HasDefaultLocaleMissingLocalesFolder) {
+ SetupUnpacker("has_default_missing_locales.crx");
+ EXPECT_FALSE(unpacker_->Run());
+ EXPECT_EQ(errors::kLocalesTreeMissing, unpacker_->error_message());
+}
+
+TEST_F(ExtensionUnpackerTest, InvalidDefaultLocale) {
+ SetupUnpacker("invalid_default_locale.crx");
+ EXPECT_FALSE(unpacker_->Run());
+ EXPECT_EQ(errors::kInvalidDefaultLocale, unpacker_->error_message());
+}
+
+TEST_F(ExtensionUnpackerTest, InvalidMessagesFile) {
+ SetupUnpacker("invalid_messages_file.crx");
+ EXPECT_FALSE(unpacker_->Run());
+ EXPECT_TRUE(MatchPattern(unpacker_->error_message(),
+ std::string("*_locales?en_US?messages.json: Line: 2, column: 3,"
+ " Dictionary keys must be quoted.")));
+}
+
+TEST_F(ExtensionUnpackerTest, MissingDefaultData) {
+ SetupUnpacker("missing_default_data.crx");
+ EXPECT_FALSE(unpacker_->Run());
+ EXPECT_EQ(errors::kLocalesNoDefaultMessages, unpacker_->error_message());
+}
+
+TEST_F(ExtensionUnpackerTest, MissingDefaultLocaleHasLocalesFolder) {
+ SetupUnpacker("missing_default_has_locales.crx");
+ EXPECT_FALSE(unpacker_->Run());
+ EXPECT_EQ(errors::kLocalesNoDefaultLocaleSpecified,
+ unpacker_->error_message());
+}
+
+TEST_F(ExtensionUnpackerTest, MissingMessagesFile) {
+ SetupUnpacker("missing_messages_file.crx");
+ EXPECT_FALSE(unpacker_->Run());
+ EXPECT_TRUE(MatchPattern(unpacker_->error_message(),
+ errors::kLocalesMessagesFileMissing +
+ std::string("*_locales?en_US?messages.json")));
+}
+
+TEST_F(ExtensionUnpackerTest, NoLocaleData) {
+ SetupUnpacker("no_locale_data.crx");
+ EXPECT_FALSE(unpacker_->Run());
+ EXPECT_EQ(errors::kLocalesTreeMissing, unpacker_->error_message());
+}
+
+TEST_F(ExtensionUnpackerTest, GoodL10n) {
+ SetupUnpacker("good_l10n.crx");
+ EXPECT_TRUE(unpacker_->Run());
+ EXPECT_TRUE(unpacker_->error_message().empty());
+ ASSERT_EQ(2U, unpacker_->parsed_catalogs()->GetSize());
+}
+
+TEST_F(ExtensionUnpackerTest, NoL10n) {
+ SetupUnpacker("no_l10n.crx");
+ EXPECT_TRUE(unpacker_->Run());
+ EXPECT_TRUE(unpacker_->error_message().empty());
+ EXPECT_EQ(0U, unpacker_->parsed_catalogs()->GetSize());
+}
diff --git a/chrome/common/render_messages_internal.h b/chrome/common/render_messages_internal.h
index 6da2ec7..b883e87 100644
--- a/chrome/common/render_messages_internal.h
+++ b/chrome/common/render_messages_internal.h
@@ -1913,11 +1913,13 @@ IPC_BEGIN_MESSAGES(ViewHost)
// because we ran out of spare message types.
// Reply when the utility process is done unpacking an extension. |manifest|
- // is the parsed manifest.json file. The unpacker should also have written
- // out a file containing decoded images from the extension. See
- // ExtensionUnpacker for details.
- IPC_MESSAGE_CONTROL1(UtilityHostMsg_UnpackExtension_Succeeded,
- DictionaryValue /* manifest */)
+ // is the parsed manifest.json file. |catalogs| is the list of all parsed
+ // message catalogs and relative paths to them.
+ // The unpacker should also have written out a file containing decoded images
+ // from the extension. See ExtensionUnpacker for details.
+ IPC_MESSAGE_CONTROL2(UtilityHostMsg_UnpackExtension_Succeeded,
+ DictionaryValue /* manifest */,
+ DictionaryValue /* catalogs */)
// Reply when the utility process has failed while unpacking an extension.
// |error_message| is a user-displayable explanation of what went wrong.
diff --git a/chrome/test/data/extensions/unpacker/empty_default_locale.crx b/chrome/test/data/extensions/unpacker/empty_default_locale.crx
new file mode 100644
index 0000000..d8f1e0e
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/empty_default_locale.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/empty_default_locale/manifest.json b/chrome/test/data/extensions/unpacker/empty_default_locale/manifest.json
new file mode 100644
index 0000000..6f43cf2
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/empty_default_locale/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "Default locale can't be empty",
+ "name": "empty_default_locale",
+ "default_locale": "",
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/good_l10n.crx b/chrome/test/data/extensions/unpacker/good_l10n.crx
new file mode 100644
index 0000000..ee56569
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/good_l10n.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/good_l10n/_locales/en_US/messages.json b/chrome/test/data/extensions/unpacker/good_l10n/_locales/en_US/messages.json
new file mode 100644
index 0000000..b5b53c6
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/good_l10n/_locales/en_US/messages.json
@@ -0,0 +1,5 @@
+{
+ "eh": {
+ "message": "this is valid catalog"
+ }
+}
diff --git a/chrome/test/data/extensions/unpacker/good_l10n/_locales/sr/messages.json b/chrome/test/data/extensions/unpacker/good_l10n/_locales/sr/messages.json
new file mode 100644
index 0000000..b5b53c6
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/good_l10n/_locales/sr/messages.json
@@ -0,0 +1,5 @@
+{
+ "eh": {
+ "message": "this is valid catalog"
+ }
+}
diff --git a/chrome/test/data/extensions/unpacker/good_l10n/manifest.json b/chrome/test/data/extensions/unpacker/good_l10n/manifest.json
new file mode 100644
index 0000000..cf2e2a9
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/good_l10n/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "All is well",
+ "name": "good_l10n",
+ "default_locale": "sr",
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/has_default_missing_locales.crx b/chrome/test/data/extensions/unpacker/has_default_missing_locales.crx
new file mode 100644
index 0000000..8d2808e
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/has_default_missing_locales.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/has_default_missing_locales/manifest.json b/chrome/test/data/extensions/unpacker/has_default_missing_locales/manifest.json
new file mode 100644
index 0000000..e572fc2
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/has_default_missing_locales/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "Cannot have default_locale but skip _locales",
+ "name": "has_default_missing_locales",
+ "default_locale": "en_US",
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/invalid_default_locale.crx b/chrome/test/data/extensions/unpacker/invalid_default_locale.crx
new file mode 100644
index 0000000..8a426c9
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/invalid_default_locale.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/invalid_default_locale/manifest.json b/chrome/test/data/extensions/unpacker/invalid_default_locale/manifest.json
new file mode 100644
index 0000000..ecf7d82
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/invalid_default_locale/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "Default locale has to be string and can't be empty",
+ "name": "invalid_default_locale",
+ "default_locale": 5,
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/invalid_messages_file.crx b/chrome/test/data/extensions/unpacker/invalid_messages_file.crx
new file mode 100644
index 0000000..15691d4
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/invalid_messages_file.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/invalid_messages_file/_locales/en_US/messages.json b/chrome/test/data/extensions/unpacker/invalid_messages_file/_locales/en_US/messages.json
new file mode 100644
index 0000000..a992f8f
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/invalid_messages_file/_locales/en_US/messages.json
@@ -0,0 +1,3 @@
+{
+ "eh:
+}
diff --git a/chrome/test/data/extensions/unpacker/invalid_messages_file/manifest.json b/chrome/test/data/extensions/unpacker/invalid_messages_file/manifest.json
new file mode 100644
index 0000000..7a4b9ef
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/invalid_messages_file/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "Fail if messages.json is not valid (check JSON syntax only)",
+ "name": "invalid_messages_file",
+ "default_locale": "en_US",
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/missing_default_data.crx b/chrome/test/data/extensions/unpacker/missing_default_data.crx
new file mode 100644
index 0000000..eeb76fe
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/missing_default_data.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/missing_default_data/_locales/en_US/messages.json b/chrome/test/data/extensions/unpacker/missing_default_data/_locales/en_US/messages.json
new file mode 100644
index 0000000..b5b53c6
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/missing_default_data/_locales/en_US/messages.json
@@ -0,0 +1,5 @@
+{
+ "eh": {
+ "message": "this is valid catalog"
+ }
+}
diff --git a/chrome/test/data/extensions/unpacker/missing_default_data/manifest.json b/chrome/test/data/extensions/unpacker/missing_default_data/manifest.json
new file mode 100644
index 0000000..03ecc4a
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/missing_default_data/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "Data for default_locale has to be present",
+ "name": "missing_default_data",
+ "default_locale": "sr",
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/missing_default_has_locales.crx b/chrome/test/data/extensions/unpacker/missing_default_has_locales.crx
new file mode 100644
index 0000000..0bf345d
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/missing_default_has_locales.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/missing_default_has_locales/manifest.json b/chrome/test/data/extensions/unpacker/missing_default_has_locales/manifest.json
new file mode 100644
index 0000000..13a5d4b
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/missing_default_has_locales/manifest.json
@@ -0,0 +1,5 @@
+{
+ "description": "If _locales is present, default_locale has to be defined too.",
+ "name": "missing_default_has_locales",
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/missing_messages_file.crx b/chrome/test/data/extensions/unpacker/missing_messages_file.crx
new file mode 100644
index 0000000..4ca3634
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/missing_messages_file.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/missing_messages_file/manifest.json b/chrome/test/data/extensions/unpacker/missing_messages_file/manifest.json
new file mode 100644
index 0000000..a3ccf4b
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/missing_messages_file/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "Each locale has to have messages.json file",
+ "name": "missing_messages_file",
+ "default_locale": "en_US",
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/no_l10n.crx b/chrome/test/data/extensions/unpacker/no_l10n.crx
new file mode 100644
index 0000000..d9edd9e
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/no_l10n.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/no_l10n/manifest.json b/chrome/test/data/extensions/unpacker/no_l10n/manifest.json
new file mode 100644
index 0000000..8ba809f
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/no_l10n/manifest.json
@@ -0,0 +1,5 @@
+{
+ "description": "This extension is not localized.",
+ "name": "no_l10n",
+ "version": "1.0"
+}
diff --git a/chrome/test/data/extensions/unpacker/no_locale_data.crx b/chrome/test/data/extensions/unpacker/no_locale_data.crx
new file mode 100644
index 0000000..30a8f40
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/no_locale_data.crx
Binary files differ
diff --git a/chrome/test/data/extensions/unpacker/no_locale_data/manifest.json b/chrome/test/data/extensions/unpacker/no_locale_data/manifest.json
new file mode 100644
index 0000000..c4d68b4
--- /dev/null
+++ b/chrome/test/data/extensions/unpacker/no_locale_data/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "_locales can't be empty",
+ "name": "no_locale_data",
+ "default_locale": "en_US",
+ "version": "1.0"
+}
diff --git a/chrome/utility/utility_thread.cc b/chrome/utility/utility_thread.cc
index 9c6912c..2c59887 100644
--- a/chrome/utility/utility_thread.cc
+++ b/chrome/utility/utility_thread.cc
@@ -31,7 +31,7 @@ void UtilityThread::OnUnpackExtension(const FilePath& extension_path) {
ExtensionUnpacker unpacker(extension_path);
if (unpacker.Run() && unpacker.DumpImagesToFile()) {
Send(new UtilityHostMsg_UnpackExtension_Succeeded(
- *unpacker.parsed_manifest()));
+ *unpacker.parsed_manifest(), *unpacker.parsed_catalogs()));
} else {
Send(new UtilityHostMsg_UnpackExtension_Failed(unpacker.error_message()));
}