summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorerikkay@google.com <erikkay@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2009-01-29 23:19:19 +0000
committererikkay@google.com <erikkay@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2009-01-29 23:19:19 +0000
commitcc65591412746641bf656d52d71663726cdd7934 (patch)
treef31a1514d566e91d00219f38d57e2cbacac988dd
parent219a862ce1d8b647364c31dfa619c4d956cfae35 (diff)
downloadchromium_src-cc65591412746641bf656d52d71663726cdd7934.zip
chromium_src-cc65591412746641bf656d52d71663726cdd7934.tar.gz
chromium_src-cc65591412746641bf656d52d71663726cdd7934.tar.bz2
Simple installation of extensions using chrome.exe --install-extensionChanged manifest filename to end in .json.Updated authoring script to include sha256 hash of zip file.
Review URL: http://codereview.chromium.org/18477 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@8926 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/browser_init.cc9
-rw-r--r--chrome/browser/extensions/extension.cc47
-rw-r--r--chrome/browser/extensions/extension.h21
-rw-r--r--chrome/browser/extensions/extension_unittest.cc13
-rw-r--r--chrome/browser/extensions/extensions_service.cc395
-rw-r--r--chrome/browser/extensions/extensions_service.h67
-rw-r--r--chrome/browser/extensions/extensions_service_unittest.cc98
-rw-r--r--chrome/common/chrome_switches.cc4
-rw-r--r--chrome/common/chrome_switches.h1
-rw-r--r--chrome/common/notification_types.h3
-rw-r--r--chrome/test/data/extensions/bad_hash.crxbin0 -> 1360 bytes
-rw-r--r--chrome/test/data/extensions/bad_json.crxbin0 -> 1360 bytes
-rw-r--r--chrome/test/data/extensions/bad_magic.crxbin0 -> 1360 bytes
-rwxr-xr-xchrome/test/data/extensions/extension1/manifest.json (renamed from chrome/test/data/extensions/extension1/manifest)2
-rwxr-xr-xchrome/test/data/extensions/extension2/manifest.json (renamed from chrome/test/data/extensions/extension2/manifest)2
-rw-r--r--chrome/test/data/extensions/good.crxbin0 -> 1360 bytes
-rw-r--r--chrome/test/data/extensions/not_an_extension.crx0
-rwxr-xr-xchrome/tools/extensions/chromium_extension.py67
18 files changed, 683 insertions, 46 deletions
diff --git a/chrome/browser/browser_init.cc b/chrome/browser/browser_init.cc
index ce4febe..2e94348 100644
--- a/chrome/browser/browser_init.cc
+++ b/chrome/browser/browser_init.cc
@@ -222,6 +222,15 @@ bool BrowserInit::LaunchWithProfile::Launch(Profile* profile,
}
}
+ // Start up the extensions service
+ profile->InitExtensions();
+ if (parsed_command_line.HasSwitch(switches::kInstallExtension)) {
+ std::wstring path_string =
+ parsed_command_line.GetSwitchValue(switches::kInstallExtension);
+ FilePath path = FilePath::FromWStringHack(path_string);
+ profile->GetExtensionsService()->InstallExtension(path);
+ }
+
return true;
}
diff --git a/chrome/browser/extensions/extension.cc b/chrome/browser/extensions/extension.cc
index b5918d5..f932eff 100644
--- a/chrome/browser/extensions/extension.cc
+++ b/chrome/browser/extensions/extension.cc
@@ -4,6 +4,7 @@
#include "chrome/browser/extensions/extension.h"
+#include "base/file_path.h"
#include "base/logging.h"
#include "base/string_util.h"
#include "net/base/net_util.h"
@@ -11,7 +12,7 @@
const char kExtensionURLScheme[] = "chrome-extension";
const char kUserScriptURLScheme[] = "chrome-user-script";
-const char Extension::kManifestFilename[] = "manifest";
+const char Extension::kManifestFilename[] = "manifest.json";
const wchar_t* Extension::kDescriptionKey = L"description";
const wchar_t* Extension::kFilesKey = L"files";
@@ -21,6 +22,7 @@ const wchar_t* Extension::kMatchesKey = L"matches";
const wchar_t* Extension::kNameKey = L"name";
const wchar_t* Extension::kUserScriptsKey = L"user_scripts";
const wchar_t* Extension::kVersionKey = L"version";
+const wchar_t* Extension::kZipHashKey = L"zip_hash";
// Extension-related error messages. Some of these are simple patterns, where a
// '*' is replaced at runtime with a specific value. This is used instead of
@@ -56,6 +58,12 @@ const char* Extension::kInvalidUserScriptsListError =
"Invalid value for 'user_scripts'.";
const char* Extension::kInvalidVersionError =
"Required value 'version' is missing or invalid.";
+const char* Extension::kInvalidZipHashError =
+ "Required key 'zip_hash' is missing or invalid.";
+
+const std::string Extension::VersionString() const {
+ return version_->GetString();
+}
// Defined in extension_protocols.h.
extern const char kExtensionURLScheme[];
@@ -149,7 +157,7 @@ bool Extension::InitFromValue(const DictionaryValue& source,
// Check format version.
int format_version = 0;
if (!source.GetInteger(kFormatVersionKey, &format_version) ||
- format_version != kExpectedFormatVersion) {
+ static_cast<uint32>(format_version) != kExpectedFormatVersion) {
*error = kInvalidFormatVersionError;
return false;
}
@@ -159,12 +167,34 @@ bool Extension::InitFromValue(const DictionaryValue& source,
*error = kInvalidIdError;
return false;
}
+ // Verify that the id is legal. This test is basically verifying that it
+ // is ASCII and doesn't have any path components in it.
+ // TODO(erikkay): verify the actual id format - it will be more restrictive
+ // than this. Perhaps just a hex string?
+ if (!IsStringASCII(id_)) {
+ *error = kInvalidIdError;
+ return false;
+ }
+ FilePath id_path;
+ id_path = id_path.AppendASCII(id_);
+ if ((id_path.value() == FilePath::kCurrentDirectory) ||
+ (id_path.value() == FilePath::kParentDirectory) ||
+ !(id_path.BaseName() == id_path)) {
+ *error = kInvalidIdError;
+ return false;
+ }
// Initialize URL.
extension_url_ = GURL(std::string(kExtensionURLScheme) + "://" + id_ + "/");
// Initialize version.
- if (!source.GetString(kVersionKey, &version_)) {
+ std::string version_str;
+ if (!source.GetString(kVersionKey, &version_str)) {
+ *error = kInvalidVersionError;
+ return false;
+ }
+ version_.reset(Version::GetVersionFromString(version_str));
+ if (!version_.get()) {
*error = kInvalidVersionError;
return false;
}
@@ -183,6 +213,16 @@ bool Extension::InitFromValue(const DictionaryValue& source,
}
}
+ // Initialize zip hash (only present in zip)
+ // There's no need to verify it at this point. If it's in a bogus format
+ // it won't pass the hash verify step.
+ if (source.HasKey(kZipHashKey)) {
+ if (!source.GetString(kZipHashKey, &zip_hash_)) {
+ *error = kInvalidZipHashError;
+ return false;
+ }
+ }
+
// Initialize user scripts (optional).
if (source.HasKey(kUserScriptsKey)) {
ListValue* list_value;
@@ -250,3 +290,4 @@ bool Extension::InitFromValue(const DictionaryValue& source,
return true;
}
+
diff --git a/chrome/browser/extensions/extension.h b/chrome/browser/extensions/extension.h
index e6a2ca8..22e8fd6 100644
--- a/chrome/browser/extensions/extension.h
+++ b/chrome/browser/extensions/extension.h
@@ -9,8 +9,10 @@
#include <vector>
#include "base/file_path.h"
+#include "base/scoped_ptr.h"
#include "base/string16.h"
#include "base/values.h"
+#include "base/version.h"
#include "chrome/browser/extensions/user_script_master.h"
#include "googleurl/src/gurl.h"
@@ -25,10 +27,11 @@ extern const char kUserScriptURLScheme[];
// Represents a Chromium extension.
class Extension {
public:
- Extension(const FilePath& path);
+ Extension() {}
+ explicit Extension(const FilePath& path);
// The format for extension manifests that this code understands.
- static const int kExpectedFormatVersion = 1;
+ static const unsigned int kExpectedFormatVersion = 1;
// The name of the manifest inside an extension.
static const char kManifestFilename[];
@@ -42,6 +45,7 @@ class Extension {
static const wchar_t* kNameKey;
static const wchar_t* kUserScriptsKey;
static const wchar_t* kVersionKey;
+ static const wchar_t* kZipHashKey;
// Error messages returned from InitFromValue().
static const char* kInvalidDescriptionError;
@@ -58,6 +62,7 @@ class Extension {
static const char* kInvalidUserScriptError;
static const char* kInvalidUserScriptsListError;
static const char* kInvalidVersionError;
+ static const char* kInvalidZipHashError;
// Creates an absolute url to a resource inside an extension. The
// |extension_url| argument should be the url() from an Extension object. The
@@ -90,7 +95,10 @@ class Extension {
const std::string& id() const { return id_; }
// The version number for the extension.
- const std::string& version() const { return version_; }
+ const Version* version() const { return version_.get(); }
+
+ // String representation of the version number.
+ const std::string VersionString() const;
// A human-readable name of the extension.
const std::string& name() const { return name_; }
@@ -117,7 +125,7 @@ class Extension {
std::string id_;
// The extension's version.
- std::string version_;
+ scoped_ptr<Version> version_;
// The extension's human-readable name.
std::string name_;
@@ -128,6 +136,11 @@ class Extension {
// Paths to the content scripts the extension contains.
UserScriptList user_scripts_;
+ // A SHA1 hash of the contents of the zip file. Note that this key is only
+ // present in the manifest that's prepended to the zip. The inner manifest
+ // will not have this key.
+ std::string zip_hash_;
+
DISALLOW_COPY_AND_ASSIGN(Extension);
};
diff --git a/chrome/browser/extensions/extension_unittest.cc b/chrome/browser/extensions/extension_unittest.cc
index 83973dd..125b939 100644
--- a/chrome/browser/extensions/extension_unittest.cc
+++ b/chrome/browser/extensions/extension_unittest.cc
@@ -25,9 +25,9 @@ TEST(ExtensionTest, InitFromValueInvalid) {
std::wstring extensions_dir;
ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_dir));
FilePath extensions_path = FilePath::FromWStringHack(extensions_dir)
- .Append(FILE_PATH_LITERAL("extensions"))
- .Append(FILE_PATH_LITERAL("extension1"))
- .Append(FILE_PATH_LITERAL("manifest"));
+ .AppendASCII("extensions")
+ .AppendASCII("extension1")
+ .AppendASCII(Extension::kManifestFilename);
JSONFileValueSerializer serializer(extensions_path.ToWStringHack());
scoped_ptr<DictionaryValue> valid_value(
@@ -99,6 +99,7 @@ TEST(ExtensionTest, InitFromValueInvalid) {
input_value.reset(static_cast<DictionaryValue*>(valid_value->DeepCopy()));
ListValue* user_scripts = NULL;
input_value->GetList(Extension::kUserScriptsKey, &user_scripts);
+ ASSERT_FALSE(NULL == user_scripts);
user_scripts->Set(0, Value::CreateIntegerValue(42));
EXPECT_FALSE(extension.InitFromValue(*input_value, &error));
EXPECT_TRUE(MatchPattern(error, Extension::kInvalidUserScriptError));
@@ -168,13 +169,13 @@ TEST(ExtensionTest, InitFromValueValid) {
// Test minimal extension
input_value.SetInteger(Extension::kFormatVersionKey, 1);
input_value.SetString(Extension::kIdKey, "com.google.myextension");
- input_value.SetString(Extension::kVersionKey, "1.0");
+ input_value.SetString(Extension::kVersionKey, "1.0.0.0");
input_value.SetString(Extension::kNameKey, "my extension");
EXPECT_TRUE(extension.InitFromValue(input_value, &error));
EXPECT_EQ("", error);
EXPECT_EQ("com.google.myextension", extension.id());
- EXPECT_EQ("1.0", extension.version());
+ EXPECT_EQ("1.0.0.0", extension.VersionString());
EXPECT_EQ("my extension", extension.name());
EXPECT_EQ("chrome-extension://com.google.myextension/",
extension.url().spec());
@@ -191,7 +192,7 @@ TEST(ExtensionTest, GetResourceURLAndPath) {
DictionaryValue input_value;
input_value.SetInteger(Extension::kFormatVersionKey, 1);
input_value.SetString(Extension::kIdKey, "com.google.myextension");
- input_value.SetString(Extension::kVersionKey, "1.0");
+ input_value.SetString(Extension::kVersionKey, "1.0.0.0");
input_value.SetString(Extension::kNameKey, "my extension");
EXPECT_TRUE(extension.InitFromValue(input_value, NULL));
diff --git a/chrome/browser/extensions/extensions_service.cc b/chrome/browser/extensions/extensions_service.cc
index e103821..78758df 100644
--- a/chrome/browser/extensions/extensions_service.cc
+++ b/chrome/browser/extensions/extensions_service.cc
@@ -5,24 +5,43 @@
#include "chrome/browser/extensions/extensions_service.h"
#include "base/file_util.h"
-#include "base/values.h"
+#include "base/scoped_handle.h"
+#include "base/scoped_temp_dir.h"
#include "base/string_util.h"
+#include "base/third_party/nss/blapi.h"
+#include "base/third_party/nss/sha256.h"
#include "base/thread.h"
+#include "base/values.h"
+#include "net/base/file_stream.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/user_script_master.h"
#include "chrome/common/json_value_serializer.h"
#include "chrome/common/notification_service.h"
+#include "chrome/common/unzip.h"
// ExtensionsService
-const FilePath::CharType* ExtensionsService::kInstallDirectoryName =
- FILE_PATH_LITERAL("Extensions");
+const char* ExtensionsService::kInstallDirectoryName = "Extensions";
+const char* ExtensionsService::kCurrentVersionFileName = "Current Version";
+const char* ExtensionsServiceBackend::kTempExtensionName = "TEMP_INSTALL";
+// Chromium Extension magic number
+static const char kExtensionFileMagic[] = "Cr24";
+
+struct ExtensionHeader {
+ char magic[sizeof(kExtensionFileMagic) - 1];
+ uint32 version;
+ size_t header_size;
+ size_t manifest_size;
+};
+
+const size_t kZipHashBytes = 32; // SHA-256
+const size_t kZipHashHexBytes = kZipHashBytes * 2; // Hex string is 2x size.
ExtensionsService::ExtensionsService(const FilePath& profile_directory,
UserScriptMaster* user_script_master)
: message_loop_(MessageLoop::current()),
backend_(new ExtensionsServiceBackend),
- install_directory_(profile_directory.Append(kInstallDirectoryName)),
+ install_directory_(profile_directory.AppendASCII(kInstallDirectoryName)),
user_script_master_(user_script_master) {
}
@@ -87,6 +106,31 @@ void ExtensionsService::OnExtensionLoadError(const std::string& error) {
LOG(WARNING) << error;
}
+void ExtensionsService::InstallExtension(const FilePath& extension_path) {
+ // TODO(aa): This message loop should probably come from a backend
+ // interface, similar to how the message loop for the frontend comes
+ // from the frontend interface.
+ g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(backend_.get(),
+ &ExtensionsServiceBackend::InstallExtension,
+ extension_path,
+ install_directory_,
+ scoped_refptr<ExtensionsServiceFrontendInterface>(this)));
+}
+
+void ExtensionsService::OnExtensionInstallError(const std::string& error) {
+ // TODO(erikkay): Print the error message out somewhere better.
+ LOG(WARNING) << error;
+}
+
+void ExtensionsService::OnExtensionInstalled(FilePath path) {
+ NotificationService::current()->Notify(NOTIFY_EXTENSION_INSTALLED,
+ NotificationService::AllSources(),
+ Details<FilePath>(&path));
+
+ // TODO(erikkay): now what?
+}
+
// ExtensionsServicesBackend
@@ -101,14 +145,14 @@ bool ExtensionsServiceBackend::LoadExtensionsFromDirectory(
// manifests. Post errors and results to the frontend.
scoped_ptr<ExtensionList> extensions(new ExtensionList);
file_util::FileEnumerator enumerator(path,
- false, // not recursive
+ false, // not recursive
file_util::FileEnumerator::DIRECTORIES);
for (FilePath child_path = enumerator.Next(); !child_path.value().empty();
child_path = enumerator.Next()) {
FilePath manifest_path =
child_path.AppendASCII(Extension::kManifestFilename);
if (!file_util::PathExists(manifest_path)) {
- ReportExtensionLoadError(frontend.get(), child_path.ToWStringHack(),
+ ReportExtensionLoadError(frontend.get(), child_path,
Extension::kInvalidManifestError);
continue;
}
@@ -117,13 +161,13 @@ bool ExtensionsServiceBackend::LoadExtensionsFromDirectory(
std::string error;
scoped_ptr<Value> root(serializer.Deserialize(&error));
if (!root.get()) {
- ReportExtensionLoadError(frontend.get(), child_path.ToWStringHack(),
+ ReportExtensionLoadError(frontend.get(), child_path,
error);
continue;
}
if (!root->IsType(Value::TYPE_DICTIONARY)) {
- ReportExtensionLoadError(frontend.get(), child_path.ToWStringHack(),
+ ReportExtensionLoadError(frontend.get(), child_path,
Extension::kInvalidManifestError);
continue;
}
@@ -131,8 +175,7 @@ bool ExtensionsServiceBackend::LoadExtensionsFromDirectory(
scoped_ptr<Extension> extension(new Extension(child_path));
if (!extension->InitFromValue(*static_cast<DictionaryValue*>(root.get()),
&error)) {
- ReportExtensionLoadError(frontend.get(), child_path.ToWStringHack(),
- error);
+ ReportExtensionLoadError(frontend.get(), child_path, error);
continue;
}
@@ -144,10 +187,12 @@ bool ExtensionsServiceBackend::LoadExtensionsFromDirectory(
}
void ExtensionsServiceBackend::ReportExtensionLoadError(
- ExtensionsServiceFrontendInterface *frontend, const std::wstring& path,
+ ExtensionsServiceFrontendInterface *frontend, const FilePath& path,
const std::string &error) {
+ // TODO(erikkay): note that this isn't guaranteed to work properly on Linux.
+ std::string path_str = WideToASCII(path.ToWStringHack());
std::string message = StringPrintf("Could not load extension from '%s'. %s",
- WideToASCII(path).c_str(), error.c_str());
+ path_str.c_str(), error.c_str());
frontend->GetMessageLoop()->PostTask(FROM_HERE, NewRunnableMethod(
frontend, &ExtensionsServiceFrontendInterface::OnExtensionLoadError,
message));
@@ -160,3 +205,329 @@ void ExtensionsServiceBackend::ReportExtensionsLoaded(
&ExtensionsServiceFrontendInterface::OnExtensionsLoadedFromDirectory,
extensions));
}
+
+// The extension file format is a header, followed by the manifest, followed
+// by the zip file. The header is a magic number, a version, the size of the
+// header, and the size of the manifest. These ints are 4 byte little endian.
+DictionaryValue* ExtensionsServiceBackend::ReadManifest(
+ const FilePath& extension_path,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend) {
+ ScopedStdioHandle file(file_util::OpenFile(extension_path, "rb"));
+ if (!file.get()) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "no such extension file");
+ return NULL;
+ }
+
+ // Read and verify the header.
+ ExtensionHeader header;
+ size_t len;
+ // TODO(erikkay): Yuck. I'm not a big fan of this kind of code, but it
+ // appears that we don't have any endian/alignment aware serialization
+ // code in the code base. So for now, this assumes that we're running
+ // on a little endian machine with 4 byte alignment.
+ len = fread(&header, 1, sizeof(ExtensionHeader), file.get());
+ if (len < sizeof(ExtensionHeader)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "invalid extension header");
+ return NULL;
+ }
+ if (strncmp(kExtensionFileMagic, header.magic, sizeof(header.magic))) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "bad magic number");
+ return NULL;
+ }
+ if (header.version != Extension::kExpectedFormatVersion) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "bad version number");
+ return NULL;
+ }
+ if (header.header_size > sizeof(ExtensionHeader))
+ fseek(file.get(), header.header_size - sizeof(ExtensionHeader), SEEK_CUR);
+
+ char buf[1 << 16];
+ std::string manifest_str;
+ size_t read_size = std::min(sizeof(buf), header.manifest_size);
+ size_t remainder = header.manifest_size;
+ while ((len = fread(buf, 1, read_size, file.get())) > 0) {
+ manifest_str.append(buf, len);
+ if (len <= remainder)
+ break;
+ remainder -= len;
+ read_size = std::min(sizeof(buf), remainder);
+ }
+
+ // Verify the JSON
+ JSONStringValueSerializer json(manifest_str);
+ std::string error;
+ Value* val = json.Deserialize(&error);
+ if (!val) {
+ ReportExtensionInstallError(frontend, extension_path, error);
+ return NULL;
+ }
+ if (!val->IsType(Value::TYPE_DICTIONARY)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "manifest isn't a JSON dictionary");
+ return NULL;
+ }
+ DictionaryValue* manifest = static_cast<DictionaryValue*>(val);
+ std::string zip_hash;
+ if (!manifest->GetString(Extension::kZipHashKey, &zip_hash)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "missing zip_hash key");
+ return NULL;
+ }
+ if (zip_hash.size() != kZipHashHexBytes) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "invalid zip_hash key");
+ return NULL;
+ }
+
+ // Read the rest of the zip file and compute a hash to compare against
+ // what the manifest claims. Compute the hash incrementally since the
+ // zip file could be large.
+ const unsigned char* ubuf = reinterpret_cast<const unsigned char*>(buf);
+ SHA256Context ctx;
+ SHA256_Begin(&ctx);
+ while ((len = fread(buf, 1, sizeof(buf), file.get())) > 0)
+ SHA256_Update(&ctx, ubuf, len);
+ uint8 hash[32];
+ SHA256_End(&ctx, hash, NULL, sizeof(hash));
+
+ std::vector<uint8> zip_hash_bytes;
+ if (!HexStringToBytes(zip_hash, &zip_hash_bytes)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "invalid zip_hash key");
+ return NULL;
+ }
+ if (zip_hash_bytes.size() != kZipHashBytes) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "invalid zip_hash key");
+ return NULL;
+ }
+ for (size_t i = 0; i < kZipHashBytes; ++i) {
+ if (zip_hash_bytes[i] != hash[i]) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "zip_hash key didn't match zip hash");
+ return NULL;
+ }
+ }
+
+ // TODO(erikkay): The manifest will also contain a signature of the hash
+ // (or perhaps the whole manifest) for authentication purposes.
+
+ return manifest;
+}
+
+bool ExtensionsServiceBackend::CheckCurrentVersion(
+ const FilePath& extension_path,
+ const std::string& version,
+ const FilePath& dest_dir,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend) {
+ FilePath current_version =
+ dest_dir.AppendASCII(ExtensionsService::kCurrentVersionFileName);
+ if (file_util::PathExists(current_version)) {
+ std::string version_str;
+ if (file_util::ReadFileToString(current_version, &version_str)) {
+ if (version_str == version) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Extension version already installed");
+ return false;
+ } else {
+ scoped_ptr<Version> cur_version(
+ Version::GetVersionFromString(version_str));
+ scoped_ptr<Version> new_version(
+ Version::GetVersionFromString(version));
+ if (cur_version->CompareTo(*new_version) >= 0) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "More recent version of extension already installed");
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool ExtensionsServiceBackend::UnzipExtension(const FilePath& extension_path,
+ const FilePath& temp_dir,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend) {
+ // <profile>/Extensions/INSTALL_TEMP/<version>
+ if (!file_util::CreateDirectory(temp_dir)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't create version directory.");
+ return false;
+ }
+ if (!Unzip(extension_path, temp_dir, NULL)) {
+ // Remove what we just installed.
+ file_util::Delete(temp_dir, true);
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't unzip extension.");
+ return false;
+ }
+ return true;
+}
+
+bool ExtensionsServiceBackend::InstallDirSafely(
+ const FilePath& extension_path,
+ const FilePath& source_dir,
+ const FilePath& dest_dir,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend) {
+
+ if (file_util::PathExists(dest_dir)) {
+ // By the time we get here, it should be safe to assume that this directory
+ // is not currently in use (it's not the current active version).
+ if (!file_util::Delete(dest_dir, true)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Can't delete existing version directory.");
+ return false;
+ }
+ } else {
+ FilePath parent = dest_dir.DirName();
+ if (!file_util::DirectoryExists(parent)) {
+ if (!file_util::CreateDirectory(parent)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't create extension directory.");
+ return false;
+ }
+ }
+ }
+ if (!file_util::Move(source_dir, dest_dir)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't move temporary directory.");
+ return false;
+ }
+
+ return true;
+}
+
+bool ExtensionsServiceBackend::SetCurrentVersion(
+ const FilePath& extension_path,
+ const FilePath& dest_dir,
+ std::string version,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend) {
+ // Write out the new CurrentVersion file.
+ // <profile>/Extension/<name>/CurrentVersion
+ FilePath current_version =
+ dest_dir.AppendASCII(ExtensionsService::kCurrentVersionFileName);
+ FilePath current_version_old =
+ current_version.InsertBeforeExtension(FILE_PATH_LITERAL("_old"));
+ if (file_util::PathExists(current_version_old)) {
+ if (!file_util::Delete(current_version_old, false)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't remove CurrentVersion_old file.");
+ return false;
+ }
+ }
+ if (file_util::PathExists(current_version)) {
+ if (!file_util::Move(current_version, current_version_old)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't move CurrentVersion file.");
+ return false;
+ }
+ }
+ net::FileStream stream;
+ int flags = base::PLATFORM_FILE_CREATE_ALWAYS | base::PLATFORM_FILE_WRITE;
+ if (stream.Open(current_version, flags) != 0)
+ return false;
+ if (stream.Write(version.c_str(), version.size(), NULL) < 0) {
+ // Restore the old CurrentVersion.
+ if (file_util::PathExists(current_version_old)) {
+ if (!file_util::Move(current_version_old, current_version)) {
+ LOG(WARNING) << "couldn't restore " << current_version_old.value() <<
+ " to " << current_version.value();
+ // TODO(erikkay): This is an ugly state to be in. Try harder?
+ }
+ }
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't create CurrentVersion file.");
+ return false;
+ }
+ return true;
+}
+
+bool ExtensionsServiceBackend::InstallExtension(
+ const FilePath& extension_path,
+ const FilePath& install_dir,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend) {
+ LOG(INFO) << "Installing extension " << extension_path.value();
+
+ // <profile>/Extensions/INSTALL_TEMP
+ FilePath temp_dir = install_dir.AppendASCII(kTempExtensionName);
+ // Ensure we're starting with a clean slate.
+ if (file_util::PathExists(temp_dir)) {
+ if (!file_util::Delete(temp_dir, true)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't delete existing temporary directory.");
+ return false;
+ }
+ }
+ ScopedTempDir scoped_temp;
+ scoped_temp.Set(temp_dir);
+ if (!scoped_temp.IsValid()) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Couldn't create temporary directory.");
+ return false;
+ }
+
+ // Read and verify the extension.
+ scoped_ptr<DictionaryValue> manifest(ReadManifest(extension_path, frontend));
+ if (!manifest.get()) {
+ // ReadManifest has already reported the extension error.
+ return false;
+ }
+ DictionaryValue* dict = manifest.get();
+ Extension extension;
+ std::string error;
+ if (!extension.InitFromValue(*dict, &error)) {
+ ReportExtensionInstallError(frontend, extension_path,
+ "Invalid extension manifest.");
+ return false;
+ }
+
+ // <profile>/Extensions/<id>
+ FilePath dest_dir = install_dir.AppendASCII(extension.id());
+ std::string version = extension.VersionString();
+ if (!CheckCurrentVersion(extension_path, version, dest_dir, frontend))
+ return false;
+
+ // <profile>/Extensions/INSTALL_TEMP/<version>
+ FilePath temp_version = temp_dir.AppendASCII(version);
+ if (!UnzipExtension(extension_path, temp_version, frontend))
+ return false;
+
+ // <profile>/Extensions/<dir_name>/<version>
+ FilePath version_dir = dest_dir.AppendASCII(version);
+ if (!InstallDirSafely(extension_path, temp_version, version_dir, frontend))
+ return false;
+
+ if (!SetCurrentVersion(extension_path, dest_dir, version, frontend)) {
+ if (!file_util::Delete(version_dir, true))
+ LOG(WARNING) << "Can't remove " << dest_dir.value();
+ return false;
+ }
+
+ ReportExtensionInstalled(frontend, dest_dir);
+ return true;
+}
+
+void ExtensionsServiceBackend::ReportExtensionInstallError(
+ ExtensionsServiceFrontendInterface *frontend, const FilePath& path,
+ const std::string &error) {
+ // TODO(erikkay): note that this isn't guaranteed to work properly on Linux.
+ std::string path_str = WideToASCII(path.ToWStringHack());
+ std::string message =
+ StringPrintf("Could not install extension from '%s'. %s",
+ path_str.c_str(), error.c_str());
+ frontend->GetMessageLoop()->PostTask(FROM_HERE, NewRunnableMethod(
+ frontend, &ExtensionsServiceFrontendInterface::OnExtensionInstallError,
+ message));
+}
+
+void ExtensionsServiceBackend::ReportExtensionInstalled(
+ ExtensionsServiceFrontendInterface *frontend, FilePath path) {
+ frontend->GetMessageLoop()->PostTask(FROM_HERE, NewRunnableMethod(
+ frontend,
+ &ExtensionsServiceFrontendInterface::OnExtensionInstalled,
+ path));
+}
diff --git a/chrome/browser/extensions/extensions_service.h b/chrome/browser/extensions/extensions_service.h
index 1a78d0a..b9cf4a6 100644
--- a/chrome/browser/extensions/extensions_service.h
+++ b/chrome/browser/extensions/extensions_service.h
@@ -33,6 +33,15 @@ class ExtensionsServiceFrontendInterface
// Called with results from LoadExtensionsFromDirectory(). The frontend
// takes ownership of the list.
virtual void OnExtensionsLoadedFromDirectory(ExtensionList* extensions) = 0;
+
+ // Install the extension file at extension_path.
+ virtual void InstallExtension(const FilePath& extension_path) = 0;
+
+ // Called when installing an extension fails.
+ virtual void OnExtensionInstallError(const std::string& message) = 0;
+
+ // Called with results from InstallExtension().
+ virtual void OnExtensionInstalled(FilePath path) = 0;
};
@@ -55,11 +64,17 @@ class ExtensionsService : public ExtensionsServiceFrontendInterface {
virtual MessageLoop* GetMessageLoop();
virtual void OnExtensionLoadError(const std::string& message);
virtual void OnExtensionsLoadedFromDirectory(ExtensionList* extensions);
+ virtual void InstallExtension(const FilePath& extension_path);
+ virtual void OnExtensionInstallError(const std::string& message);
+ virtual void OnExtensionInstalled(FilePath path);
+
+ // The name of the file that the current active version number is stored in.
+ static const char* kCurrentVersionFileName;
private:
// The name of the directory inside the profile where extensions are
// installed to.
- static const FilePath::CharType* kInstallDirectoryName;
+ static const char* kInstallDirectoryName;
// The message loop for the thread the ExtensionsService is running on.
MessageLoop* message_loop_;
@@ -96,16 +111,64 @@ class ExtensionsServiceBackend
const FilePath &path,
scoped_refptr<ExtensionsServiceFrontendInterface> frontend);
+ // Install the extension file at extension_path to install_dir.
+ // ReportExtensionInstallError is called on error.
+ // ReportExtensionInstalled is called on success.
+ bool InstallExtension(
+ const FilePath& extension_path,
+ const FilePath& install_dir,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend);
+
private:
// Notify a frontend that there was an error loading an extension.
void ReportExtensionLoadError(ExtensionsServiceFrontendInterface* frontend,
- const std::wstring& path,
+ const FilePath& path,
const std::string& error);
// Notify a frontend that extensions were loaded.
void ReportExtensionsLoaded(ExtensionsServiceFrontendInterface* frontend,
ExtensionList* extensions);
+ // Notify a frontend that there was an error installing an extension.
+ void ReportExtensionInstallError(ExtensionsServiceFrontendInterface* frontend,
+ const FilePath& path,
+ const std::string& error);
+
+ // Notify a frontend that extensions were installed.
+ void ReportExtensionInstalled(ExtensionsServiceFrontendInterface* frontend,
+ FilePath path);
+
+ // Read the manifest from the front of the extension file.
+ DictionaryValue* ReadManifest(const FilePath& extension_path,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend);
+
+ // Check that the version to be installed is > the current installed
+ // extension.
+ bool CheckCurrentVersion(const FilePath& extension_path,
+ const std::string& version,
+ const FilePath& dest_dir,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend);
+
+ // Unzip the extension into |dest_dir|.
+ bool UnzipExtension(const FilePath& extension_path,
+ const FilePath& dest_dir,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend);
+
+ // Install the extension dir by moving it from |source| to |dest| safely.
+ bool InstallDirSafely(const FilePath& extension_path,
+ const FilePath& source, const FilePath& dest,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend);
+
+ // Update the CurrentVersion file in |dest_dir| to |version|.
+ bool SetCurrentVersion(const FilePath& extension_path,
+ const FilePath& dest_dir,
+ std::string version,
+ scoped_refptr<ExtensionsServiceFrontendInterface> frontend);
+
+ // The name of a temporary directory to install an extension into for
+ // validation before finalizing install.
+ static const char* kTempExtensionName;
+
DISALLOW_COPY_AND_ASSIGN(ExtensionsServiceBackend);
};
diff --git a/chrome/browser/extensions/extensions_service_unittest.cc b/chrome/browser/extensions/extensions_service_unittest.cc
index bfc5664..03b0b24 100644
--- a/chrome/browser/extensions/extensions_service_unittest.cc
+++ b/chrome/browser/extensions/extensions_service_unittest.cc
@@ -32,6 +32,12 @@ struct ExtensionsOrder {
class ExtensionsServiceTestFrontend
: public ExtensionsServiceFrontendInterface {
public:
+
+ ExtensionsServiceTestFrontend() {
+ file_util::CreateNewTempDirectory(FILE_PATH_LITERAL("ext_test"),
+ &install_dir_);
+ }
+
~ExtensionsServiceTestFrontend() {
for (ExtensionList::iterator iter = extensions_.begin();
iter != extensions_.end(); ++iter) {
@@ -47,6 +53,14 @@ class ExtensionsServiceTestFrontend
return &extensions_;
}
+ std::vector<FilePath>* installed() {
+ return &installed_;
+ }
+
+ FilePath install_dir() {
+ return install_dir_;
+ }
+
// ExtensionsServiceFrontendInterface
virtual MessageLoop* GetMessageLoop() {
return &message_loop_;
@@ -65,10 +79,43 @@ class ExtensionsServiceTestFrontend
std::stable_sort(extensions_.begin(), extensions_.end(), ExtensionsOrder());
}
+ virtual void InstallExtension(const FilePath& extension_path) {
+ }
+
+ virtual void OnExtensionInstallError(const std::string& message) {
+ errors_.push_back(message);
+ }
+
+ virtual void OnExtensionInstalled(FilePath path) {
+ installed_.push_back(path);
+ }
+
+ void TestInstallExtension(const FilePath& path,
+ ExtensionsServiceBackend* backend,
+ bool should_succeed) {
+ ASSERT_TRUE(file_util::PathExists(path));
+ EXPECT_EQ(should_succeed,
+ backend->InstallExtension(path, install_dir_,
+ scoped_refptr<ExtensionsServiceFrontendInterface>(this)));
+ message_loop_.RunAllPending();
+ if (should_succeed) {
+ EXPECT_EQ(1u, installed_.size());
+ EXPECT_EQ(0u, errors_.size());
+ } else {
+ EXPECT_EQ(0u, installed_.size());
+ EXPECT_EQ(1u, errors_.size());
+ }
+ installed_.clear();
+ errors_.clear();
+ }
+
+
private:
MessageLoop message_loop_;
ExtensionList extensions_;
std::vector<std::string> errors_;
+ std::vector<FilePath> installed_;
+ FilePath install_dir_;
};
// make the test a PlatformTest to setup autorelease pools properly on mac
@@ -76,10 +123,9 @@ typedef PlatformTest ExtensionsServiceTest;
// Test loading extensions from the profile directory.
TEST_F(ExtensionsServiceTest, LoadAllExtensionsFromDirectory) {
- std::wstring extensions_dir;
- ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_dir));
- FilePath extensions_path = FilePath::FromWStringHack(extensions_dir).Append(
- FILE_PATH_LITERAL("extensions"));
+ FilePath extensions_path;
+ ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path));
+ extensions_path = extensions_path.AppendASCII("extensions");
scoped_refptr<ExtensionsServiceBackend> backend(new ExtensionsServiceBackend);
scoped_refptr<ExtensionsServiceTestFrontend> frontend(
@@ -108,11 +154,11 @@ TEST_F(ExtensionsServiceTest, LoadAllExtensionsFromDirectory) {
EXPECT_EQ(2u, scripts[0].matches.size());
EXPECT_EQ("http://*.google.com/*", scripts[0].matches[0]);
EXPECT_EQ("https://*.google.com/*", scripts[0].matches[1]);
- EXPECT_EQ(extension->path().Append(FILE_PATH_LITERAL("script1.js")).value(),
+ EXPECT_EQ(extension->path().AppendASCII("script1.js").value(),
scripts[0].path.value());
EXPECT_EQ(1u, scripts[1].matches.size());
EXPECT_EQ("http://*.yahoo.com/*", scripts[1].matches[0]);
- EXPECT_EQ(extension->path().Append(FILE_PATH_LITERAL("script2.js")).value(),
+ EXPECT_EQ(extension->path().AppendASCII("script2.js").value(),
scripts[1].path.value());
EXPECT_EQ(std::string("com.google.myextension2"),
@@ -123,3 +169,43 @@ TEST_F(ExtensionsServiceTest, LoadAllExtensionsFromDirectory) {
frontend->extensions()->at(1)->description());
ASSERT_EQ(0u, frontend->extensions()->at(1)->user_scripts().size());
};
+
+// Test installing extensions.
+TEST_F(ExtensionsServiceTest, InstallExtension) {
+ FilePath extensions_path;
+ ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &extensions_path));
+ extensions_path = extensions_path.AppendASCII("extensions");
+
+ scoped_refptr<ExtensionsServiceBackend> backend(new ExtensionsServiceBackend);
+ scoped_refptr<ExtensionsServiceTestFrontend> frontend(
+ new ExtensionsServiceTestFrontend);
+
+ FilePath path = extensions_path.AppendASCII("good.crx");
+
+ // A simple extension that should install without error.
+ frontend->TestInstallExtension(path, backend, true);
+ // TODO(erikkay): verify the contents of the installed extension.
+
+ // Installing the same extension twice should fail.
+ frontend->TestInstallExtension(path, backend, false);
+
+ // 0-length extension file.
+ path = extensions_path.AppendASCII("not_an_extension.crx");
+ frontend->TestInstallExtension(path, backend, false);
+
+ // Bad magic number.
+ path = extensions_path.AppendASCII("bad_magic.crx");
+ frontend->TestInstallExtension(path, backend, false);
+
+ // Poorly formed JSON.
+ path = extensions_path.AppendASCII("bad_json.crx");
+ frontend->TestInstallExtension(path, backend, false);
+
+ // Incorrect zip hash.
+ path = extensions_path.AppendASCII("bad_hash.crx");
+ frontend->TestInstallExtension(path, backend, false);
+
+ // TODO(erikkay): add more tests for many of the failure cases.
+ // TODO(erikkay): add tests for upgrade cases.
+}
+
diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc
index a156748..f2e0c68 100644
--- a/chrome/common/chrome_switches.cc
+++ b/chrome/common/chrome_switches.cc
@@ -340,6 +340,10 @@ const wchar_t kEnableUserScripts[] = L"enable-user-scripts";
// Enable extensions.
const wchar_t kEnableExtensions[] = L"enable-extensions";
+// Install the extension specified in the argument. This is for MIME type
+// handling so that users can double-click on an extension.
+const wchar_t kInstallExtension[] = L"install-extension";
+
// Causes the browser to launch directly in incognito mode.
const wchar_t kIncognito[] = L"incognito";
diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h
index 65a7e00..046c690 100644
--- a/chrome/common/chrome_switches.h
+++ b/chrome/common/chrome_switches.h
@@ -130,6 +130,7 @@ extern const wchar_t kSdchFilter[];
extern const wchar_t kEnableUserScripts[];
extern const wchar_t kEnableExtensions[];
+extern const wchar_t kInstallExtension[];
extern const wchar_t kIncognito[];
extern const wchar_t kUseOldSafeBrowsing[];
diff --git a/chrome/common/notification_types.h b/chrome/common/notification_types.h
index e6e2e2e..c07a3aa 100644
--- a/chrome/common/notification_types.h
+++ b/chrome/common/notification_types.h
@@ -511,6 +511,9 @@ enum NotificationType {
// Sent when new extensions are loaded. The details are an ExtensionList*.
NOTIFY_EXTENSIONS_LOADED,
+ // Sent when new extensions are installed. The details are a FilePath.
+ NOTIFY_EXTENSION_INSTALLED,
+
// Count (must be last) ------------------------------------------------------
// Used to determine the number of notification types. Not valid as
// a type parameter when registering for or posting notifications.
diff --git a/chrome/test/data/extensions/bad_hash.crx b/chrome/test/data/extensions/bad_hash.crx
new file mode 100644
index 0000000..9959e9c
--- /dev/null
+++ b/chrome/test/data/extensions/bad_hash.crx
Binary files differ
diff --git a/chrome/test/data/extensions/bad_json.crx b/chrome/test/data/extensions/bad_json.crx
new file mode 100644
index 0000000..cc15c0e
--- /dev/null
+++ b/chrome/test/data/extensions/bad_json.crx
Binary files differ
diff --git a/chrome/test/data/extensions/bad_magic.crx b/chrome/test/data/extensions/bad_magic.crx
new file mode 100644
index 0000000..4603e00
--- /dev/null
+++ b/chrome/test/data/extensions/bad_magic.crx
Binary files differ
diff --git a/chrome/test/data/extensions/extension1/manifest b/chrome/test/data/extensions/extension1/manifest.json
index a7b3d74..1f5e362 100755
--- a/chrome/test/data/extensions/extension1/manifest
+++ b/chrome/test/data/extensions/extension1/manifest.json
@@ -1,7 +1,7 @@
{
"format_version": 1,
"id": "com.google.myextension1",
- "version": "1.0",
+ "version": "1.0.0.0",
"name": "My extension 1",
"description": "The first extension that I made.",
"user_scripts": [
diff --git a/chrome/test/data/extensions/extension2/manifest b/chrome/test/data/extensions/extension2/manifest.json
index 0f58ffd..4e157c4 100755
--- a/chrome/test/data/extensions/extension2/manifest
+++ b/chrome/test/data/extensions/extension2/manifest.json
@@ -1,6 +1,6 @@
{
"format_version": 1,
"id": "com.google.myextension2",
- "version": "1.0",
+ "version": "1.0.0.0",
"name": "My extension 2"
}
diff --git a/chrome/test/data/extensions/good.crx b/chrome/test/data/extensions/good.crx
new file mode 100644
index 0000000..c2108e2
--- /dev/null
+++ b/chrome/test/data/extensions/good.crx
Binary files differ
diff --git a/chrome/test/data/extensions/not_an_extension.crx b/chrome/test/data/extensions/not_an_extension.crx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/chrome/test/data/extensions/not_an_extension.crx
diff --git a/chrome/tools/extensions/chromium_extension.py b/chrome/tools/extensions/chromium_extension.py
index 1151652..de5d3bf 100755
--- a/chrome/tools/extensions/chromium_extension.py
+++ b/chrome/tools/extensions/chromium_extension.py
@@ -5,17 +5,22 @@
# chromium_extension.py
+import array
+import hashlib
import logging
import optparse
import os
import re
import shutil
+import simplejson as json
import sys
import zipfile
ignore_dirs = [".svn", "CVS"]
ignore_files = [re.compile(".*~")]
+MANIFEST_FILENAME = "manifest.json"
+
class ExtensionDir:
def __init__(self, path):
self._root = os.path.abspath(path)
@@ -34,8 +39,8 @@ class ExtensionDir:
self._files.append(os.path.join(root, f))
def validate(self):
- if os.path.join(self._root, "manifest") not in self._files:
- logging.error("package is missing a valid manifest")
+ if os.path.join(self._root, MANIFEST_FILENAME) not in self._files:
+ logging.error("package is missing a valid %s file" % MANIFEST_FILENAME)
return False
return True
@@ -43,21 +48,61 @@ class ExtensionDir:
if not self.validate():
return False
try:
- if os.path.exists(path):
- os.remove(path)
- shutil.copy(os.path.join(self._root, "manifest"), path)
- # This is a bit odd - we're actually appending a new zip file to the end
- # of the manifest. Believe it or not, this is actually an explicit
- # feature of the zipfile package, and most zip utilities (this library
- # and three others I tried) can still read the underlying zip file.
- zip = zipfile.ZipFile(path, "a")
+ f = open(os.path.join(self._root, MANIFEST_FILENAME))
+ manifest = json.load(f)
+ f.close()
+
+ zip_path = path + ".zip"
+ if os.path.exists(zip_path):
+ os.remove(zip_path)
+ zip = zipfile.ZipFile(zip_path, "w")
(root, dir) = os.path.split(self._root)
- root_len = len(root)
+ root_len = len(self._root)
for file in self._files:
arcname = file[root_len+1:]
logging.debug("%s: %s" % (arcname, file))
zip.write(file, arcname)
zip.close()
+
+ zip = open(zip_path, mode="rb")
+ hash = hashlib.sha256()
+ while True:
+ buf = zip.read(32 * 1024)
+ if not len(buf):
+ break
+ hash.update(buf)
+ zip.close()
+
+ manifest["zip_hash"] = hash.hexdigest()
+
+ # This is a bit odd - we're actually appending a new zip file to the end
+ # of the manifest. Believe it or not, this is actually an explicit
+ # feature of the zip format, and many zip utilities (this library
+ # and three others I tried) can still read the underlying zip file.
+ if os.path.exists(path):
+ os.remove(path)
+ out = open(path, "wb")
+ out.write("Cr24") # Extension file magic number
+ # The rest of the header is currently made up of three ints:
+ # version, header size, manifest size
+ header = array.array("l")
+ header.append(1) # version
+ header.append(16) # header size
+ manifest_json = json.dumps(manifest);
+ header.append(len(manifest_json)) # manifest size
+ header.tofile(out)
+ out.write(manifest_json);
+ zip = open(zip_path, "rb")
+ while True:
+ buf = zip.read(32 * 1024)
+ if not len(buf):
+ break
+ out.write(buf)
+ zip.close()
+ out.close()
+
+ os.remove(zip_path)
+
logging.info("created extension package %s" % path)
except IOError, (errno, strerror):
logging.error("error creating extension %s (%d, %s)" % (path, errno,