diff -r 000000000000 -r ca70ae20a155 src/tools/py2sis/ensymble/sisfile.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/tools/py2sis/ensymble/sisfile.py Tue Feb 16 10:07:05 2010 +0530 @@ -0,0 +1,643 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################## +# sisfile.py - Symbian OS v9.x SIS file utilities +# Copyright 2006, 2007 Jussi Ylänen +# +# This file is part of Ensymble developer utilities for Symbian OS(TM). +# +# Ensymble is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Ensymble is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ensymble; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +############################################################################## + +import os +import time +import struct +import sha + +import symbianutil +import cryptutil +import sisfield + + +############################################################################## +# Public module-level functions +############################################################################## + +def parseexpression(expr): + '''Create a SISExpression SISField out of the given expression string. + + parseexpression(...) -> SISExpression + + expr the expression, a string + + SISExpression the returned SISExpression SISField + + NOTE: Only expressions of form "language == nn" are supported, for now.''' + + elist = expr.lower().split() + + # TODO: Only "language == nn" expressions supported for now. + # Going to need a real parser for general expression support, though. + try: + if len(elist) != 3 or elist[0] != 'language' or elist[1] != '==': + raise ValueError + langnum = int(elist[2]) + except ValueError: + raise ValueError("invalid expression '%s'" % expr) + + # Create a SISExpression SISField of type EPrimTypeVariable. + leftfield = sisfield.SISExpression(Operator = sisfield.EPrimTypeVariable, + IntegerValue = sisfield.EVarLanguage) + + # Create a SISExpression SISField of type EPrimTypeNumber. + rightfield = sisfield.SISExpression(Operator = sisfield.EPrimTypeNumber, + IntegerValue = langnum) + + # Create an equality test SISExpression SISField and return it. + return sisfield.SISExpression(Operator = sisfield.EBinOpEqual, + IntegerValue = 0, + LeftExpression = leftfield, + RightExpression = rightfield) + +def signstring(privkey, passphrase, string): + '''Sign a binary string using a given private key and its pass phrase. + + signstring(...) -> (signature, algorithm oid) + + privkey private key (RSA or DSA), a binary string in PEM format + passphrase pass phrase (non-Unicode) for the private key or None + string binary string from which the signature is to be calculated + + signature signature, a binary string + algorithm oid signature algorithm object identifier, a string''' + + # Sign string. + signature, keytype = cryptutil.signstring(privkey, passphrase, string) + + # Determine algorithm object identifier. + if keytype == "DSA": + algoid = "1.2.840.10040.4.3" + elif keytype == "RSA": + algoid = "1.2.840.113549.1.1.5" + else: + raise ValueError("unknown key type '%s'" % keytype) + + return (signature, algoid) + + +############################################################################## +# Module-level functions which are normally only used by this module +############################################################################## + +def makefiledata(contents): + '''Make a SISFileData SISField out of the given binary string. + + makefiledata(...) -> SISFileData + + contents file contents, a binary string + + SISFileData the returned SISFileData instance + + NOTE: Data is compressed only if it is beneficial.''' + + # Wrap data inside SISCompressed SISField. + cfield = sisfield.SISCompressed(Data = contents, + CompressionAlgorithm = sisfield.ECompressAuto, + rawdatainside = True) + + # Create a SISFileData SISField out of the wrapped data and return it. + return sisfield.SISFileData(FileData = cfield) + +def makefiledesc(contents, compressedlen, index, target = None, + mimetype = None, capabilities = None, + operation = sisfield.EOpInstall, options = 0): + '''Make a SISFileDescription SISField for the given file. + + makefiledesc(...) -> SISFileDescription + + contents file contents for SHA-1 digest calc., a binary string + compressedlen length of file contents inside a SISCompressed SISField + index index of file inside a SISDataUnit SISField, an integer + target install path in target device, a string or None + mimetype MIME type, a string or None + capabilities Symbian OS capabilities for EXE-files, int. mask or None + operation what to do with the file, an integer bit mask + options operation dependent install options, an integer bit mask + + SISFileDescription the returned SISFileDescription instance + + Constants for operation and options can be found in the sisfield module. + Operation is one of EOpInstall, EOpRun, EOpText or EOpNull. Options + depend on the selected operation, for example EInstVerifyOnRestore.''' + + # Create a SISString of target path. + if target == None: + # Target may be None. The file is not installed anywhere in that case. + target = "" + targetfield = sisfield.SISString(String = target) + + # Create a SISString of MIME type. + if mimetype == None: + # MIME type may be None (and usually is). + mimetype = "" + mimetypefield = sisfield.SISString(String = mimetype) + + # Create a SISCapabilities SISField for executable capabilities. + if capabilities != None and capabilities != 0L: + # SISCapabilities expects a binary string, need to convert the + # capability mask. If capability mask is 0, no capability field + # is generated. Otherwise signsis.exe cannot sign the resulting + # SIS file. + capstring = symbianutil.capmasktorawdata(capabilities) + capfield = sisfield.SISCapabilities(Capabilities = capstring) + else: + # Only EXE- and DLL-files have a concept of capability. + capfield = None + + # Calculate file hash using SHA-1. Create a SISHash SISField out of it. + # Contents may be None, to properly support the EOpNull install operation. + if contents != None: + sha1hash = sha.new(contents).digest() + else: + # No data, the containing SISBlob is mandatory but empty. + sha1hash = "" + hashblob = sisfield.SISBlob(Data = sha1hash) + hashfield = sisfield.SISHash(HashAlgorithm = sisfield.ESISHashAlgSHA1, + HashData = hashblob) + + # Create a SISFileDescription SISField and return it. + return sisfield.SISFileDescription(Target = targetfield, + MIMEType = mimetypefield, + Capabilities = capfield, + Hash = hashfield, + Operation = operation, + OperationOptions = options, + Length = compressedlen, + UncompressedLength = len(contents), + FileIndex = index) + +def makedependency(uid, fromversion, toversion, names): + '''Make a SISDependency SISField for the given UID, version dependency. + + makedependency(...) -> SISDependency + + uid UID, an unsigned integer + fromversion from-version, a triple-item list/tuple (major, minor, build) + toversion to-version, a triple-item list/tuple or None + names names for the dependency, a list of string per language + + SISDependency the returned SISDependency SISField + + NOTE: toversion may be None, indicating any version after fromversion.''' + + # Convert parameters to SISFields. + uidfield = sisfield.SISUid(UID1 = uid) + + fromverfield = sisfield.SISVersion(Major = fromversion[0], + Minor = fromversion[1], + Build = fromversion[2]) + if toversion != None: + toverfield = sisfield.SISVersion(Major = toversion[0], + Minor = toversion[1], + Build = toversion[2]) + else: + toverfield = None + + verrangefield = sisfield.SISVersionRange(FromVersion = fromverfield, + ToVersion = toverfield) + + l = [] + for name in names: + l.append(sisfield.SISString(String = name)) + namesfield = sisfield.SISArray(SISFields = l, SISFieldType = "SISString") + + # Create a SISDependency SISField and return it. + return sisfield.SISDependency(UID = uidfield, + VersionRange = verrangefield, + DependencyNames = namesfield) + + +def makeinstallblock(files = [], embeddedsisfiles = [], ifblocks = []): + '''Make a SISInstallBlock SISField out of the given lists of SISFields. + + makeinstallblock(...) -> SISInstallBlock + + files a list of SISFileDescription SISFields (normal files) + embeddedsisfiles a list of SISController SISFields (embedded SIS files) + ifblocks a list of SISIf SISFields (conditionally installed files) + + SISInstallBlock the returned SISInstallBlock instance + + NOTE: Any of the lists may be empty (and are, by default).''' + + + # Convert lists to SISArrays. + sa1 = sisfield.SISArray(SISFields = files, + SISFieldType = "SISFileDescription") + sa2 = sisfield.SISArray(SISFields = embeddedsisfiles, + SISFieldType = "SISController") + sa3 = sisfield.SISArray(SISFields = ifblocks, + SISFieldType = "SISIf") + + # Create a SISInstallBlock SISField and return it. + return sisfield.SISInstallBlock(Files = sa1, EmbeddedSISFiles = sa2, + IfBlocks = sa3) + +def makelangconditional(languages, langdepfiles): + '''Make a SISIf and SISElseIfs for language dependent installation of files. + + makelangconditional(...) -> SISIf or None + + languages a list of language numbers (not names, IDs or SISLanguages) + landepfiles a list of file lists, where each file list is a list of + alternative SISFileDescription SISFields for each language + + SISIf the returned SISIf instance or None if no files''' + + if len(langdepfiles) == 0: + # No language dependent files, leave. + return None + + # Create a file list per language. + filesperlang = [] + for n in xrange(len(languages)): + filesperlang.append([]) + + # Gather all files from the same language to a single list. + for files in langdepfiles: + if len(files) != len(languages): + raise ValueError("%d files given but number of languages is %d" % + (len(files), len(languages))) + + for n in xrange(len(languages)): + filesperlang[n].append(files[n]) + + if len(languages) == 0: + # No languages, leave. (This is down here so that errors + # can still be caught above.) + return None + + # Create a SISArray of SISElseIf SISFields. + elseiffields = [] + for n in xrange(1, len(languages)): + elseifexpfield = parseexpression("language == %d" % languages[n]) + elseiffield = sisfield.SISElseIf(Expression = elseifexpfield, + InstallBlock = makeinstallblock(filesperlang[n])) + elseiffields.append(elseiffield) + elseiffieldarray = sisfield.SISArray(SISFields = elseiffields, + SISFieldType = "SISElseIf") + + # Create and return the final SISIf SISField. + ifexpfield = parseexpression("language == %d" % languages[0]) + return sisfield.SISIf(Expression = ifexpfield, + InstallBlock = makeinstallblock(filesperlang[0]), + ElseIfs = elseiffieldarray) + + +############################################################################## +# SimpleSISWriter class for no-frills SIS file generation +############################################################################## + +class SimpleSISWriter(object): + '''A no-frills SIS file generator + + Limitations: + + - Option lists are not supported. + - Condition blocks are not supported. Languages are, however. + - Nested SIS files are not supported. + - SIS type is always a full installation package (type EInstInstallation). + - Package options (EInstFlagShutdownApps) are not supported.''' + + def __init__(self, languages, names, uid, version, + vendorname, vendornames, creationtime = None): + # Set empty list of languages, names, files, certificates and so on. + self.languages = [] + self.filedata = [] + self.files = [] + self.langdepfiles = [] + self.logo = None + self.certificates = [] + self.targetdevices = [] + self.dependencies = [] + self.properties = [] + + # Convert language IDs/names to language numbers. + for lang in languages: + try: + langnum = symbianutil.langidtonum[lang] + except KeyError: + # Not a language ID, try names next. + try: + langnum = symbianutil.langnametonum[lang] + except KeyError: + raise ValueError("invalid language '%s'" % lang) + self.languages.append(langnum) + + # Verify number of names and vendor names wrt. number of languages. + if len(names) != len(self.languages): + raise ValueError( + "%d package names given but number of languages is %d" % + (len(names), len(self.languages))) + + if len(vendornames) != len(self.languages): + raise ValueError( + "%d vendor names given but number of languages is %d" % + (len(vendornames), len(self.languages))) + + # Convert language dependent names to a SISArray of SISStrings. + l = [] + for name in names: + l.append(sisfield.SISString(String = name)) + self.names = sisfield.SISArray(SISFields = l, + SISFieldType = "SISString") + + # Convert integer UID to SISUid SISField. + self.uid = sisfield.SISUid(UID1 = uid) + + # Convert version number triplet to SISVersion SISField. + self.version = sisfield.SISVersion(Major = version[0], + Minor = version[1], + Build = version[2]) + + # Convert unique vendor name to SISString SISField. + self.vendorname = sisfield.SISString(String = vendorname) + + # Convert language dependent vendor names to a SISArray of SISStrings. + l = [] + for name in vendornames: + l.append(sisfield.SISString(String = name)) + self.vendornames = sisfield.SISArray(SISFields = l, + SISFieldType = "SISString") + + if creationtime == None: + # If no creation time given, use the time + # of SimpleSISWriter instantiation. + creationtime = time.gmtime() + + # Convert standard Python time representation to SISFields. + datefield = sisfield.SISDate(Year = creationtime.tm_year, + Month = creationtime.tm_mon - 1, + Day = creationtime.tm_mday) + timefield = sisfield.SISTime(Hours = creationtime.tm_hour, + Minutes = creationtime.tm_min, + Seconds = creationtime.tm_sec) + self.creationtime = sisfield.SISDateTime(Date = datefield, + Time = timefield) + + def setlogo(self, contents, mimetype): + '''Add a logo graphics to generated SIS file. + + NOTE: Not all devices display a logo during installation.''' + + if self.logo != None: + raise ValueError("logo already set") + + # Create SISFileData and SISFileDescription SISFields. + filedata = makefiledata(contents) + complen = filedata.getcompressedlength() + runopts = (sisfield.EInstFileRunOptionInstall | + sisfield.EInstFileRunOptionByMimeType) + filedesc = makefiledesc(contents, complen, len(self.filedata), + None, mimetype, None, sisfield.EOpRun, runopts) + self.logo = sisfield.SISLogo(LogoFile = filedesc) + self.filedata.append(filedata) + + def addfile(self, contents, target = None, mimetype = None, + capabilities = None, operation = sisfield.EOpInstall, + options = 0): + '''Add a file that is same for all languages to generated SIS file.''' + + # Create SISFileData and SISFileDescription SISFields. + filedata = makefiledata(contents) + complen = filedata.getcompressedlength() + metadata = makefiledesc(contents, complen, len(self.filedata), + target, mimetype, capabilities, + operation, options) + self.files.append(metadata) + self.filedata.append(filedata) + + def addlangdepfile(self, clist, target = None, mimetype = None, + capabilities = None, operation = sisfield.EOpInstall, + options = 0): + '''Add language dependent files to generated SIS file. + + A conditional expression is automatically generated for the file.''' + + if len(clist) != len(self.languages): + raise ValueError("%d files given but number of languages is %d" % + (len(clist), len(self.languages))) + + data = [] + files = [] + index = len(self.filedata) + for contents in clist: + # Create SISFileData and SISFileDescription SISFields. + filedata = makefiledata(contents) + complen = filedata.getcompressedlength() + metadata = makefiledesc(contents, complen, index, + target, mimetype, capabilities, + operation, options) + files.append(metadata) + data.append(filedata) + index += 1 + + self.langdepfiles.append(files) + self.filedata.extend(data) + + def addcertificate(self, privkey, cert, passphrase): + '''Add a certificate to SIS file. + + Private key and certificate are in PEM (base-64) format.''' + + self.certificates.append((privkey, cert, passphrase)) + + def addtargetdevice(self, uid, fromversion, toversion, names): + '''Add a mandatory target device UID to generated SIS file. + + NOTE: Names are not usually displayed. Instead, the device vendor + has specified what the names must be.''' + + if len(names) != len(self.languages): + raise ValueError( + "%d device names given but number of languages is %d" % + (len(names), len(self.languages))) + + depfield = makedependency(uid, fromversion, toversion, names) + self.targetdevices.append(depfield) + + def adddependency(self, uid, fromversion, toversion, names): + '''Add an installed package dependency to generated SIS file. + + NOTE: Some devices display the first name of the dependency + regardless of the device language.''' + + if len(names) != len(self.languages): + raise ValueError( + "%d dependency names given but number of languages is %d" % + (len(names), len(self.languages))) + + depfield = makedependency(uid, fromversion, toversion, names) + self.dependencies.append(depfield) + + def addproperty(self, key, value): + '''Add a property key, value pair to generated SIS file. + + When installing other SIS files, they may query these properties.''' + + # Convert parameters to a SISProperty SISField. + self.properties.append(sisfield.SISProperty(Key = key, + Value = value)) + + def tostring(self): + '''Convert this SIS instance to a (possibly very large) string.''' + + # Generate a SISInfo SISField. + infofield = sisfield.SISInfo(UID = self.uid, + VendorUniqueName = self.vendorname, + Names = self.names, + VendorNames = self.vendornames, + Version = self.version, + CreationTime = self.creationtime, + InstallType = sisfield.EInstInstallation, + InstallFlags = 0) + + # Generate an empty SISSupportedOptions SISField. + # Option lists are not supported by SimpleSISWriter. + sa = sisfield.SISArray(SISFields = [], + SISFieldType = "SISSupportedOption") + optfield = sisfield.SISSupportedOptions(Options = sa) + + # Convert language numbers to SISArray of SISLanguages + # and generate a SISSupportedLanguages SISField. + langfieldlist = [] + for lang in self.languages: + langfieldlist.append(sisfield.SISLanguage(Language = lang)) + sa = sisfield.SISArray(SISFields = langfieldlist, + SISFieldType = "SISLanguage") + langfield = sisfield.SISSupportedLanguages(Languages = sa) + + # Generate SISPrerequisites SISField. + sa1 = sisfield.SISArray(SISFields = self.targetdevices, + SISFieldType = "SISDependency") + sa2 = sisfield.SISArray(SISFields = self.dependencies, + SISFieldType = "SISDependency") + prereqfield = sisfield.SISPrerequisites(TargetDevices = sa1, + Dependencies = sa2) + + # Generate SISProperties SISField. + sa = sisfield.SISArray(SISFields = self.properties, + SISFieldType = "SISProperty") + propfield = sisfield.SISProperties(Properties = sa) + + # Generate SISInstallBlock SISField. + iffield = makelangconditional(self.languages, self.langdepfiles) + if iffield: + # Some language dependent files + iffieldlist = [iffield] + else: + # No language dependent files + iffieldlist = [] + ibfield = makeinstallblock(self.files, [], iffieldlist) + + # Generate a data index field. No embedded SIS files, index is 0. + didxfield = sisfield.SISDataIndex(DataIndex = 0) + + # Generate a SISController SISField without any signatures. + ctrlfield = sisfield.SISController(Info = infofield, + Options = optfield, + Languages = langfield, + Prerequisites = prereqfield, + Properties = propfield, + Logo = self.logo, + InstallBlock = ibfield) + + # Calculate metadata signature for each certificate. + certfieldlist = [] + for cert in self.certificates: + # Calculate a signature of the SISController so far. + string = ctrlfield.tostring() + string = sisfield.stripheaderandpadding(string) + signature, algoid = signstring(cert[0], cert[2], string) + + # Create a SISCertificateChain SISField from certificate data. + sf1 = sisfield.SISBlob(Data = cryptutil.certtobinary(cert[1])) + sf2 = sisfield.SISCertificateChain(CertificateData = sf1) + + # Create a SISSignature SISField from calculated signature. + sf3 = sisfield.SISString(String = algoid) + sf4 = sisfield.SISSignatureAlgorithm(AlgorithmIdentifier = sf3) + sf5 = sisfield.SISBlob(Data = signature) + sf6 = sisfield.SISSignature(SignatureAlgorithm = sf4, + SignatureData = sf5) + + # Create a new SISSignatureCertificateChain SISField. + sa = sisfield.SISArray(SISFields = [sf6]) + certfieldlist.append(sisfield.SISSignatureCertificateChain( + Signatures = sa, CertificateChain = sf2)) + + # Add certificate to SISController SISField. + ctrlfield.setsignatures(certfieldlist) + + # Finally add a data index field to SISController SISField. + # and wrap it in SISCompressed SISField. + ctrlfield.DataIndex = didxfield + ctrlcompfield = sisfield.SISCompressed(Data = ctrlfield, + CompressionAlgorithm = sisfield.ECompressDeflate) + + # Generate SISData SISField. + sa = sisfield.SISArray(SISFields = self.filedata, + SISFieldType = "SISFileData") + dufield = sisfield.SISDataUnit(FileData = sa) + sa = sisfield.SISArray(SISFields = [dufield]) + datafield = sisfield.SISData(DataUnits = sa) + + # Calculate SISController checksum. + # TODO: Requires an extra tostring() conversion. + ctrlcs = symbianutil.crc16ccitt(ctrlcompfield.tostring()) + ctrlcsfield = sisfield.SISControllerChecksum(Checksum = ctrlcs) + + # Calculate SISData checksum. + # TODO: Requires an extra tostring() conversion. + datacs = symbianutil.crc16ccitt(datafield.tostring()) + datacsfield = sisfield.SISDataChecksum(Checksum = datacs) + + # Generate SISContents SISField. + contentsfield = sisfield.SISContents(ControllerChecksum = ctrlcsfield, + DataChecksum = datacsfield, + Controller = ctrlcompfield, + Data = datafield) + + # Generate a SIS UID string. + uidstring = symbianutil.uidstostring(0x10201a7aL, 0x00000000L, + self.uid.UID1) + + # Return the completed SIS file as a string. + return uidstring + contentsfield.tostring() + + def tofile(self, outfile): + '''Write this SIS instance to a file object or a named file.''' + + s = self.tostring() + + try: + f = file(outfile, "wb") + try: + f.write(s) + finally: + f.close() + except TypeError: + f.write(s)