#!/usr/bin/python3 # Copyright (c) 2006-2008 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. # chromium_extension.py import array import hashlib import logging import optparse import os import random import re import shutil import sys import zipfile if sys.version_info < (2, 6): import simplejson as json else: import json ignore_dirs = [".svn", "CVS"] ignore_files = [re.compile(".*~")] MANIFEST_FILENAME = "manifest.json" class ExtensionDir: def __init__(self, path): self._root = os.path.abspath(path) self._dirs = [] self._files = [] for root, dirs, files in os.walk(path, topdown=True): for dir in ignore_dirs: if dir in dirs: dirs.remove(dir) root = os.path.abspath(root) for dir in dirs: self._dirs.append(os.path.join(root, dir)) for f in files: for match in ignore_files: if not match.match(f): self._files.append(os.path.join(root, f)) def validate(self): 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 def writeToPackage(self, path): if not self.validate(): return False try: f = open(os.path.join(self._root, MANIFEST_FILENAME), "r") manifest = json.load(f) f.close() # Temporary hack: If the manifest doesn't have an ID, generate a random # one. This is to make it easier for people to play with the extension # system while we don't have the real ID mechanism in place. if not "id" in manifest: random_id = "" for i in range(0, 40): random_id += "0123456789ABCDEF"[random.randrange(0, 15)] logging.info("Generated extension ID: %s" % random_id) manifest["id"] = random_id; f = open(os.path.join(self._root, MANIFEST_FILENAME), "w") f.write(json.dumps(manifest, sort_keys=True, indent=2)); 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(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(b"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("i") header.append(1) # version header.append(16) # header size manifest_json = json.dumps(manifest); header.append(len(manifest_json)) # manifest size header.tofile(out) import codecs out.write(codecs.encode(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 as err: (errno, strerror) = err.args logging.error("error creating extension %s (%d, %s)" % (path, errno, strerror)) try: if os.path.exists(path): os.remove(path) except: pass return False return True class ExtensionPackage: def __init__(self, path): zip = zipfile.ZipFile(path) error = zip.testzip() if error: logging.error("error reading extension: %s", error) return logging.info("%s contents:" % path) files = zip.namelist() for f in files: logging.info(f) def Run(): logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") parser = optparse.OptionParser("usage: %prog --indir= --outfile=") parser.add_option("", "--indir", help="an input directory where the extension lives") parser.add_option("", "--outfile", help="extension package filename to create") (options, args) = parser.parse_args() if not options.indir: parser.error("missing required option --indir") if not options.outfile: parser.error("missing required option --outfile") ext = ExtensionDir(options.indir) ext.writeToPackage(options.outfile) pkg = ExtensionPackage(options.outfile) return 0 if __name__ == "__main__": retcode = Run() sys.exit(retcode)