# Copyright 2008 The Tor Project, Inc.  See LICENSE for licensing information.

import time
import re
import binascii
import calendar
import os

import thandy.checkJson
import thandy.util

json = thandy.util.importJSON()

import Crypto.Hash.SHA256

class KeyDB:
    """A KeyDB holds public keys, indexed by their key IDs."""
    ## Fields:
    #   _keys: a map from keyid to public key.
    def __init__(self):
        """Create a new empty KeyDB."""
        self._keys = {}
    def addKey(self, k):
        """Insert a thandy.keys.PublicKey object, 'k', into this KeyDB.  If
           we already had this key, retain the old one, but add any roles in
           the new key 'k'.
        """
        keyid = k.getKeyID()
        try:
            oldkey = self._keys[keyid]
            for r, p in oldkey.getRoles():
                if (r, p) not in k.getRoles():
                    k.addRole(r,p)
        except KeyError:
            pass
        self._keys[k.getKeyID()] = k
    def getKey(self, keyid):
        """Return the key whose key ID is 'keyid'.  If there is no such key,
           raise KeyError."""
        return self._keys[keyid]
    def getKeysByRole(self, role, path):
        """Return a list of all keys that have the role 'role' set for files
           in 'path'."""
        results = []
        for key in self._keys.itervalues():
            for r,p in key.getRoles():
                if r == role:
                    if rolePathMatches(p, path):
                        results.append(key)
        return results

    def getKeysFuzzy(self, keyid):
        """Return a list of all keys whose key IDs begin with 'keyid'."""
        r = []
        for k,v in self._keys.iteritems():
            if k.startswith(keyid):
                r.append(v)
        return r
    def iterkeys(self):
        """Return a new iterator of all the keys in this KeyDB."""
        return self._keys.itervalues()

# Internal cache that maps role paths to regex objects that parse them.
_rolePathCache = {}
def rolePathMatches(rolePath, path):
    """Return true iff the relative path in the filesystem 'path' conforms
       to the pattern 'rolePath': a path that a given key is
       authorized to sign.  Patterns are allowed to contain * to
       represent one or more characters in a filename, and ** to
       represent any level of directory structure.

    >>> rolePathMatches("a/b/c/", "a/b/c/")
    True
    >>> rolePathMatches("**/c.*", "a/b/c.txt")
    True
    >>> rolePathMatches("**/c.*", "a/b/ctxt")
    False
    >>> rolePathMatches("**/c.*", "a/b/c.txt/foo")
    False
    >>> rolePathMatches("a/*/c", "a/b/c")
    True
    >>> rolePathMatches("a/*/c", "a/b/c.txt")
    False
    >>> rolePathMatches("a/*/c", "a/b/c.txt") #Check cache
    False
    """
    try:
        regex = _rolePathCache[rolePath]
    except KeyError:
        orig = rolePath
        # remove duplicate slashes.
        rolePath = re.sub(r'/+', '/', rolePath)
        # escape, then ** becomes .*
        rolePath = re.escape(rolePath).replace(r'\*\*', r'.*')
        # * becomes [^/]*
        rolePath = rolePath.replace(r'\*', r'[^/]*')
        # and no extra text is allowed.
        rolePath += "$"
        regex = _rolePathCache[orig] = re.compile(rolePath)
    return regex.match(path) != None

class SignatureStatus:
    """Represents the outcome of checking signature(s) on an object."""
    def __init__(self, good, bad, unrecognized, unauthorized):
        # keyids for all the valid signatures
        self._good = good[:]
        # keyids for the invalid signatures (we had the key, and it failed).
        self._bad = bad[:]
        # keyids for signatures where we didn't recognize the key
        self._unrecognized = unrecognized[:]
        # keyids for signatures where we recognized the key, but it doesn't
        # seem to be allowed to sign this kind of document.
        self._unauthorized = unauthorized[:]

    def isValid(self, threshold=1):
        """Return true iff we got at least 'threshold' good signatures."""
        return len(self._good) >= threshold

    def mayNeedNewKeys(self):
        """Return true iff downloading a new set of keys might tip this
           signature status over to 'valid.'"""
        return len(self._unrecognized) or len(self._unauthorized)

def checkSignatures(signed, keyDB, role=None, path=None):
    """Given an object conformant to SIGNED_SCHEMA and a set of public keys
       in keyDB, verify the signed object is signed.  If 'role' and 'path'
       are provided, verify that the signing key has the correct role to
       sign this document as stored in 'path'.

       Returns a SignatureStatus.
    """

    SIGNED_SCHEMA.checkMatch(signed)

    goodSigs = []
    badSigs = []
    unknownSigs = []
    tangentialSigs = []

    signable = signed['signed']
    signatures = signed['signatures']

    d_obj = Crypto.Hash.SHA256.new()
    getDigest(signable, d_obj)
    digest = d_obj.digest()

    for signature in signatures:
        sig = signature['sig']
        keyid = signature['keyid']
        method = signature['method']

        try:
            key = keyDB.getKey(keyid)
        except KeyError:
            unknownSigs.append(keyid)
            continue

        try:
            result = key.checkSignature(method, sig, digest=digest)
        except thandy.UnknownMethod:
            continue

        if result:
            if role is not None:
                for r,p in key.getRoles():
                    if r == role and rolePathMatches(p, path):
                        break
                else:
                    tangentialSigs.append(sig)
                    continue

            goodSigs.append(keyid)
        else:
            badSigs.append(keyid)

    return SignatureStatus(goodSigs, badSigs, unknownSigs, tangentialSigs)

def _canonical_str_encoder(s):
    """Helper for encodeCanonical: encodes a string as the byte sequence
       expected for canonical JSON format.
    """
    s = '"%s"' % re.sub(r'(["\\])', r'\\\1', s)
    if isinstance(s, unicode):
        return s.encode("utf-8")
    else:
        return s

def _encodeCanonical(obj, outf):
    # Helper for encodeCanonical.  Older versions of json.encoder don't
    # even let us replace the separators.

    if isinstance(obj, basestring):
        outf(_canonical_str_encoder(obj))
    elif obj is True:
        outf("true")
    elif obj is False:
        outf("false")
    elif obj is None:
        outf("null")
    elif isinstance(obj, (int,long)):
        outf(str(obj))
    elif isinstance(obj, (tuple, list)):
        outf("[")
        if len(obj):
            for item in obj[:-1]:
                _encodeCanonical(item, outf)
                outf(",")
            _encodeCanonical(obj[-1], outf)
        outf("]")
    elif isinstance(obj, dict):
        outf("{")
        if len(obj):
            items = obj.items()
            items.sort()
            for k,v in items[:-1]:
                outf(_canonical_str_encoder(k))
                outf(":")
                _encodeCanonical(v, outf)
                outf(",")
            k, v = items[-1]
            outf(_canonical_str_encoder(k))
            outf(":")
            _encodeCanonical(v, outf)
        outf("}")
    else:
        raise thandy.FormatException("I can't encode %r"%obj)

def encodeCanonical(obj, outf=None):
    """Encode the object obj in canoncial JSon form, as specified at
       http://wiki.laptop.org/go/Canonical_JSON .  It's a restricted
       dialect of JSON in which keys are always lexically sorted,
       there is no whitespace, floats aren't allowed, and only quote
       and backslash get escaped.  The result is encoded in UTF-8, and
       the resulting bytes are passed to outf (if provided) in several
       calls, or joined into a string and returned.

       >>> encodeCanonical("")
       '""'
       >>> encodeCanonical([1, 2, 3])
       '[1,2,3]'
       >>> encodeCanonical([])
       '[]'
       >>> encodeCanonical({"A": [99]})
       '{"A":[99]}'
       >>> encodeCanonical({"x" : 3, "y" : 2})
       '{"x":3,"y":2}'
       >>> total = 0
       >>> def increment(s):
       ...   global total
       ...   total += len(s)
       ...
       >>> encodeCanonical({"x" : 3, "y" : 2, 'z' : [99,3]}, outf=increment)
       >>> total
       24
    """

    result = None
    if outf == None:
        result = [ ]
        outf = result.append

    _encodeCanonical(obj, outf)

    if result is not None:
        return "".join(result)

def getDigest(obj, digestObj=None):
    """Update 'digestObj' (typically a SHA256 object) with the digest of
       obj, first encoding it in canonical form if it's a JSON object,
       and taking its UTF-8 encoding if it's in unicode.  If digestObj
       is none, just compute and return the SHA256 hash.
    """
    useTempDigestObj = (digestObj == None)
    if useTempDigestObj:
        digestObj = Crypto.Hash.SHA256.new()

    if isinstance(obj, str):
        digestObj.update(obj)
    elif isinstance(obj, unicode):
        digestObj.update(obj.encode("utf-8"))
    else:
        encodeCanonical(obj, digestObj.update)

    if useTempDigestObj:
        return digestObj.digest()

def getFileDigest(f, digestObj=None):
    """Update 'digestObj' (typically a SHA256 object) with the digest
       of the file object (or filename) in f.  If digestObj is none,
       compute the SHA256 hash and return it.

       >>> s = "here is a long string"*1000
       >>> import cStringIO, Crypto.Hash.SHA256
       >>> h1 = Crypto.Hash.SHA256.new()
       >>> h2 = Crypto.Hash.SHA256.new()
       >>> getFileDigest(cStringIO.StringIO(s), h1)
       >>> h2.update(s)
       >>> h1.digest() == h2.digest()
       True
    """
    f_to_close = None
    if isinstance(f, basestring):
        f_to_close = f = open(f, 'rb')

    useTempDigestObj = (digestObj == None)
    if useTempDigestObj:
        digestObj = Crypto.Hash.SHA256.new()

    try:
        while 1:
            s = f.read(4096)
            if not s:
                break
            digestObj.update(s)
    finally:
        if f_to_close != None:
            f_to_close.close()

    if useTempDigestObj:
        return digestObj.digest()

def makeSignable(obj):
    """Return a new JSON object of type 'signed' wrapping 'obj', and containing
       no signatures.
    """
    return { 'signed' : obj, 'signatures' : [] }

def sign(signed, key):
    """Add an element to the signatures of 'signed', containing a new signature
       of the "signed" part using 'key'.  Replaces all previous signatures
       generated with 'key'.
    """

    SIGNED_SCHEMA.checkMatch(signed)

    signable = signed["signed"]
    signatures = signed['signatures']

    keyid = key.getKeyID()

    signatures = [ s for s in signatures if s['keyid'] != keyid ]
    newsignatures = key.sign(signable)

    for method, sig in newsignatures:
        signatures.append({ 'keyid' : keyid,
                            'method' : method,
                            'sig' : sig })

    signed['signatures'] = signatures

def formatTime(t):
    """Encode the time 't' in YYYY-MM-DD HH:MM:SS format.

    >>> formatTime(1221265172)
    '2008-09-13 00:19:32'
    """
    return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(t))

def parseTime(s):
    """Parse a time 's' in YYYY-MM-DD HH:MM:SS format."""
    try:
        return calendar.timegm(time.strptime(s, "%Y-%m-%d %H:%M:%S"))
    except ValueError:
        raise thandy.FormatException("Malformed time %r", s)

def formatBase64(h):
    """Return the base64 encoding of h with whitespace and = signs omitted."""
    return binascii.b2a_base64(h).rstrip("=\n ")

formatHash = formatBase64

def parseBase64(s):
    """Parse a base64 encoding with whitespace and = signs omitted. """
    extra = len(s) % 4
    if extra:
        padding = "=" * (4 - extra)
        s += padding
    try:
        return binascii.a2b_base64(s)
    except binascii.Error:
        raise thandy.FormatException("Invalid base64 encoding")

def parseHash(s):
    """Parse a base64-encoded digest.

       (This is just like paseBase64, but it checks the size.)
    """
    h = parseBase64(s)
    if len(h) != Crypto.Hash.SHA256.digest_size:
        raise thandy.FormatException("Bad hash length")
    return h

# Abbreviate the thandy.checkJson module here, since we're going to be
# using all of its members a lot here.
S = thandy.checkJson

#########
## These schemas describe, in OO constraint-checking form, all the Thandy
## data formats.

# A date, in YYYY-MM-DD HH:MM:SS format.
TIME_SCHEMA = S.RE(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')
# A hash, base64-encoded
HASH_SCHEMA = S.RE(r'[a-zA-Z0-9\+\/]{43}')

# A hexadecimal value.
HEX_SCHEMA = S.RE(r'[a-fA-F0-9]+')
# A base-64 encoded value
BASE64_SCHEMA = S.RE(r'[a-zA-Z0-9\+\/]+')
# An RSA key; subtype of PUBKEY_SCHEMA.
RSAKEY_SCHEMA = S.Obj(
    _keytype=S.Str("rsa"),
    e=BASE64_SCHEMA,
    n=BASE64_SCHEMA)
# An RSA key with private-key informartion: subtype of RSAKEY_SCHEMA.
RSAKEY_PRIVATE_SCHEMA = S.Obj(
    _keytype=S.Str("rsa"),
    e=BASE64_SCHEMA,
    n=BASE64_SCHEMA,
    d=BASE64_SCHEMA,
    p=BASE64_SCHEMA,
    q=BASE64_SCHEMA,
    u=BASE64_SCHEMA)
# Any public key.
PUBKEY_SCHEMA = S.Obj(
    _keytype=S.AnyStr())

KEYID_SCHEMA = HASH_SCHEMA
SIG_METHOD_SCHEMA = S.AnyStr()
RELPATH_SCHEMA = PATH_PATTERN_SCHEMA = S.AnyStr()
URL_SCHEMA = S.AnyStr()
VERSION_SCHEMA = S.ListOf(S.Any()) #XXXX WRONG
LENGTH_SCHEMA = S.Int(lo=0)

# A single signature of an object.  Indicates the signature, the id of the
# signing key, and the signing method.
SIGNATURE_SCHEMA = S.Obj(
    keyid=KEYID_SCHEMA,
    method=SIG_METHOD_SCHEMA,
    sig=BASE64_SCHEMA)

# A signed object.
SIGNED_SCHEMA = S.Obj(
    signed=S.Any(),
    signatures=S.ListOf(SIGNATURE_SCHEMA))

# The name of a role
ROLENAME_SCHEMA = S.AnyStr()

# A role: indicates that a key is allowed to certify a kind of
# document at a certain place in the repo.
ROLE_SCHEMA = S.Struct([ROLENAME_SCHEMA, PATH_PATTERN_SCHEMA], allowMore=True)

# A Keylist: indicates a list of live keys and their roles.
KEYLIST_SCHEMA = S.Obj(
    _type=S.Str("Keylist"),
    ts=TIME_SCHEMA,
    keys=S.ListOf(S.Obj(key=PUBKEY_SCHEMA, roles=S.ListOf(ROLE_SCHEMA))))

# A Mirrorlist: indicates all the live mirrors, and what documents they
# serve.
MIRRORLIST_SCHEMA = S.Obj(
    _type=S.Str("Mirrorlist"),
    ts=TIME_SCHEMA,
    mirrors=S.ListOf(S.Obj(name=S.AnyStr(),
                           urlbase=URL_SCHEMA,
                           contents=S.ListOf(PATH_PATTERN_SCHEMA),
                           weight=S.Int(lo=0),
                           )))

# A timestamp: indicates the lastest versions of all top-level signed objects.
TIMESTAMP_SCHEMA = S.Obj(
    _type = S.Str("Timestamp"),
    at = TIME_SCHEMA,
    m = S.Struct([TIME_SCHEMA, HASH_SCHEMA], [LENGTH_SCHEMA], allowMore=True),
    k = S.Struct([TIME_SCHEMA, HASH_SCHEMA], [LENGTH_SCHEMA], allowMore=True),
    b = S.DictOf(keySchema=S.AnyStr(),
            valSchema=
                 S.Struct([ VERSION_SCHEMA, RELPATH_SCHEMA, TIME_SCHEMA, HASH_SCHEMA ], [LENGTH_SCHEMA], allowMore=True))
    )

# A Bundle: lists a bunch of packages that should be updated in tandem
BUNDLE_SCHEMA = S.Obj(
   _type=S.Str("Bundle"),
   at=TIME_SCHEMA,
   name=S.AnyStr(),
   os=S.AnyStr(),
   arch=S.Opt(S.AnyStr()),
   version=VERSION_SCHEMA,
   location=RELPATH_SCHEMA,
   packages=S.ListOf(S.Obj(
                    name=S.AnyStr(),
                    version=VERSION_SCHEMA,
                    path=RELPATH_SCHEMA,
                    hash=HASH_SCHEMA,
                    length=LENGTH_SCHEMA,
                    order=S.Struct([S.Int(), S.Int(), S.Int()]),
                    optional=S.Opt(S.Bool()),
                    gloss=S.DictOf(S.AnyStr(), S.AnyStr()),
                    longgloss=S.DictOf(S.AnyStr(), S.AnyStr()))))

def checkWinRegistryKeyname(keyname):
    """Check keyname for superficial well-formedness as a win32 registry entry
       name."""
    hkey, rest = keyname.split("\\", 1)
    key, value = rest.rsplit("\\", 1)
    if hkey not in [ "HKEY_CURRENT_CONFIG",
                     "HKEY_CURRENT_USER",
                     "HKEY_LOCAL_MACHINE" ]:
        raise thandy.FormatException("Bad hkey on registry entry.")
    elif not key or not value:
        raise thandy.FormatException("Bad registry entry.")

# A string holding the name of a windows registry key
REGISTRY_KEY_SCHEMA = S.Func(checkWinRegistryKeyname)

CHECK_ITEM_SCHEMA = S.TaggedObj(
    tagName='check_type',
    tagIsOptional=True,
    registry=S.Obj(registry_ent=S.Struct([REGISTRY_KEY_SCHEMA, S.AnyStr()])),
    db=S.Obj(item_name=S.AnyStr(),
             item_version=S.Any() #XXXX wrong!
             ),
    rpm=S.Obj(rpm_version=S.AnyStr()))

INSTALL_ITEM_SCHEMA = S.TaggedObj(
    tagName='install_type',
    tagIsOptional=True,
    command=S.Obj(cmd_install=S.ListOf(S.AnyStr()),
                  cmd_remove=S.Opt(S.ListOf(S.AnyStr()))),
    rpm=S.Obj())

OBSOLETE_EXE_FORMAT_ITEM_SCHEMA = S.Obj(
    registry_ent=S.Opt(S.Struct([REGISTRY_KEY_SCHEMA, S.AnyStr()])),
    exe_args=S.ListOf(S.AnyStr()))
OBSOLETE_RPM_FORMAT_ITEM_SCHEMA = S.Obj(
    rpm_version=S.AnyStr())

ITEM_INFO_SCHEMA = S.AllOf([CHECK_ITEM_SCHEMA, INSTALL_ITEM_SCHEMA])

ITEM_SCHEMA = S.Struct([RELPATH_SCHEMA, HASH_SCHEMA],
                       [ITEM_INFO_SCHEMA, LENGTH_SCHEMA],
                       allowMore=True)

def checkPackageFormatConsistency(obj):
    format = obj.get('format')
    if format:
        formatSchema = { 'exe' : OBSOLETE_EXE_FORMAT_ITEM_SCHEMA,
                         'rpm' : OBSOLETE_RPM_FORMAT_ITEM_SCHEMA }.get(format)
        if formatSchema:
            for f in obj['files']:
                if len(f) >= 3:
                    formatSchema.checkMatch(f[2])

PACKAGE_SCHEMA = S.Obj(
            _type=S.Str("Package"),
            name=S.AnyStr(),
            location=RELPATH_SCHEMA,
            version=VERSION_SCHEMA,
            format=S.Opt(S.AnyStr()),
            ts=TIME_SCHEMA,
            files=S.ListOf(S.Struct([RELPATH_SCHEMA, HASH_SCHEMA],
                                    allowMore=True)),
            shortdesc=S.DictOf(S.AnyStr(), S.AnyStr()),
            longdesc=S.DictOf(S.AnyStr(), S.AnyStr()))

THP_PACKAGE_SCHEMA = S.Obj(
            format_version=S.Any(),
            manifest=S.ListOf(S.Obj(name=RELPATH_SCHEMA,
                                    digest=HASH_SCHEMA,
                                    is_config=S.Bool())),
            package_name=S.AnyStr(),
            package_version=S.AnyStr(),
            package_version_tuple=VERSION_SCHEMA,
            timestamp=TIME_SCHEMA,
            additional_files=S.Opt(S.ListOf(S.AnyStr())),
            install_order=S.Opt(S.Int()),
            options=S.Opt(S.DictOf(S.AnyStr(), S.Any())),
            platform=S.Opt(S.DictOf(S.AnyStr(), S.AnyStr())),
            require_features=S.Opt(S.ListOf(S.AnyStr())),
            require_packages=S.Opt(S.ListOf(S.ListOf(S.AnyStr()))),
            scripts=S.Opt(S.DictOf(S.AnyStr(),
              S.ListOf(S.Struct([S.AnyStr(), S.ListOf(S.AnyStr())])))))

PACKAGE_SCHEMA = S.Func(checkPackageFormatConsistency, PACKAGE_SCHEMA)

ALL_ROLES = ('timestamp', 'mirrors', 'bundle', 'package', 'master')

class Keylist(KeyDB):
    """A list of keys, as extracted from a Thandy keys.txt JSon file.

       This class extends KeyDB, so you can acces keys more easily.
    """
    def __init__(self):
        KeyDB.__init__(self)

    def addFromKeylist(self, obj, allowMasterKeys=False):
        for keyitem in obj['keys']:
            key = keyitem['key']
            roles = keyitem['roles']

            try:
                key = thandy.keys.RSAKey.fromJSon(key)
            except thandy.FormatException, e:
                print e
                #LOG skipping key.
                continue

            for r,p in roles:
                if r == 'master' and not allowMasterKeys:
                    #LOG
                    continue
                if r not in ALL_ROLES:
                    continue
                key.addRole(r,p)

            self.addKey(key)

class StampedInfo:
    """This class holds a single entry in a timestamp file.  Each
       StampedInfo says when a file was last modified, and what its
       hash was.  It may also provide useful info about where to find it,
       its version, its length, and so on.
    """
    ## _ts -- the time when the file was last modified
    ## _hash -- the hash of the most recent version of the file
    ## _version -- version of the most recent file. May be None
    ## _relpath -- where to find this file in the repository
    ## _length -- the length of the file
    def __init__(self, ts, hash, version=None, relpath=None, length=None):
        self._ts = ts
        self._hash = hash
        self._version = version
        self._relpath = relpath
        self._length = length

    @staticmethod
    def fromJSonFields(timeStr, hashStr, length=None):
        t = parseTime(timeStr)
        h = parseHash(hashStr)
        return StampedInfo(t, h, length=length)

    def getHash(self):
        return self._hash

    def getRelativePath(self):
        return self._relpath

    def getLength(self):
        return self._length

class TimestampFile:
    """This class holds all the fields parsed from a thandy timestamp file."""
    ## _time -- the time when this file was generated
    ## _mirrorListInfo -- a StampedInfo for the keylist.
    ## _keyListInfo -- a StampedInfo for the mirrorlist
    ## _bundleInfo -- map from bundle name to StampedInfo
    def __init__(self, at, mirrorlistinfo, keylistinfo, bundleinfo):
        self._time = at
        self._mirrorListInfo = mirrorlistinfo
        self._keyListInfo = keylistinfo
        self._bundleInfo = bundleinfo

    @staticmethod
    def fromJSon(obj):
        # must be validated.
        at = parseTime(obj['at'])
        # We slice these lists because we want to support old thandys
        # that didn't include the length on these, and new ones that
        # might include more fields
        m = StampedInfo.fromJSonFields(*obj['m'][:3])
        k = StampedInfo.fromJSonFields(*obj['k'][:3])
        b = {}
        for name, bundle in obj['b'].iteritems():
            v = bundle[0]
            rp = bundle[1]
            t = parseTime(bundle[2])
            h = parseHash(bundle[3])
            ln = None
            if len(bundle) > 4:
                ln = bundle[4]
            b[name] = StampedInfo(t, h, v, rp, ln)

        return TimestampFile(at, m, k, b)

    def getTime(self):
        return self._time

    def getMirrorlistInfo(self):
        return self._mirrorListInfo

    def getKeylistInfo(self):
        return self._keyListInfo

    def getBundleInfo(self, name):
        return self._bundleInfo[name]

    def getBundleInfos(self):
        return self._bundleInfo

def readConfigFile(fname, needKeys=(), optKeys=(), preload={}):
    """Read a configuration file from 'fname'.  A configuration file is a
       python script that runs in a temporary namespace prepopulated
       with the contents of 'reload'.  It is a thandy.FormatException
       if the file finishes executation without setting every variable
       listed in 'needKeys'.  These settings, plus any variables whose names
       are listed in 'optKeys', are returned in a new dict.
    """
    parsed = preload.copy()
    result = {}
    execfile(fname, parsed)

    for k in needKeys:
        try:
            result[k] = parsed[k]
        except KeyError:
            raise thandy.FormatException("Missing value for %s in %s" % (k, fname))

    for k in optKeys:
        try:
            result[k] = parsed[k]
        except KeyError:
            pass

    return result

def makePackageObj(config_fname, package_fname):
    """Given a description of a thandy package in config_fname, and the
       name of the one file (only one is supported now!) in package_fname,
       return a new unsigned package object.
    """
    preload = {}
    shortDescs = {}
    longDescs = {}
    def ShortDesc(lang, val): shortDescs[lang] = val
    def LongDesc(lang, val): longDescs[lang] = val
    #XXXX handle multiple files.
    preload = { 'ShortDesc' : ShortDesc, 'LongDesc' : LongDesc }
    r = readConfigFile(config_fname,
                       ['name',
                        'version',
                        'format',
                        'location',
                        'relpath',
                        ], ['rpm_version', 'exe_args',
                            'exe_registry_ent',
                            'db_key', 'db_val',
                            'command_install', 'command_remove',
                            'thp_name', 'thp_version', 'thp_dest'
                            ], preload)

    f = open(package_fname, 'rb')
    digest = getFileDigest(f)
    f.close()
    f_len = os.stat(package_fname).st_size

    # Check fields!
    extra = {}
    result = { '_type' : "Package",
               'ts' : formatTime(time.time()),
               'name' : r['name'],
               'location' : r['location'], #DOCDOC
               'version' : r['version'],
               'format' : r['format'],
               'files' : [ [ r['relpath'], formatHash(digest), extra, f_len ] ],
               'shortdesc' : shortDescs,
               'longdesc' : longDescs
             }

    format = r['format']
    if format == 'rpm':
        if not r.get('rpm_version'):
            raise thandy.FormatException("missing rpm_version value")
        extra['rpm_version'] = r['rpm_version']
        extra['check_type'] = 'rpm'
        extra['install_type'] = 'rpm'
    elif format == 'exe':
        if not r.get('exe_args'):
            raise thandy.FormatException("missing exe_args value")
        extra['exe_args'] = r['exe_args']
        if not r.get('cmd_install'):
            extra['install_type'] = 'command'
            extra['cmd_install'] = [ "${FILE}" ] + r['exe_args']
    elif format == 'thp':
        extra['check_type'] = 'thp'
        extra['install_type'] = 'thp'
        if not r.get('thp_name'):
            raise thandy.FormatException("missing thp_name value")
        extra['thp_name'] = r['thp_name']
        if not r.get('thp_version'):
            raise thandy.FormatException("missing thp_version value")
        extra['thp_version'] = r['thp_version']
        if not r.get('thp_dest'):
            extra['thp_dest'] = ""
        else:
            extra['thp_dest'] = r['thp_dest']

    if r.get('command_install'):
        extra['install_type'] = 'command'
        extra['cmd_install'] = r['command_install']
        if r.get('command_remove'):
            extra['cmd_remove'] = r['command_remove']

    if r.get('exe_registry_ent'):
        if len(r['exe_registry_ent']) != 2:
            raise thandy.FormatException("Bad length on exe_registry_ent")
        regkey, regval = r['exe_registry_ent']
        checkWinRegistryKeyname(regkey)
        if not isinstance(regval, basestring):
            raise thandy.FormatException("Bad version on exe_registry_ent")
        extra['registry_ent'] = [ regkey, regval ]
        extra['check_type'] = 'registry'
    elif r.get('db_key'):
        extra['item_name'] = r['db_key']
        extra['item_version'] = r['db_val']
        extra['check_type'] = 'db'

    PACKAGE_SCHEMA.checkMatch(result)

    return result

def makeThpPackageObj(config_fname, package_path):
    """Given a description of a thandy Thp package in config_fname, and the
       name of the directory where the installable contests are in
       package_fname, return a new unsigned package object.
    """
    preload = {}
    optKeys = ['additional_files',
               'install_order',
               'options',
               'platform',
               'require_features',
               'require_packages',
               'scripts'
              ]

    r = readConfigFile(config_fname,
                       ['format_version',
                        'files',
                        'package_name',
                        'package_version',
                        'package_version_tuple',
                        ], optKeys, preload)

    file_list = []
    for (file, is_config) in r["files"]:
        f = open(os.path.join(package_path, file), 'rb')
        digest = getFileDigest(f)
        f.close()

        file_list.append({'name' : file,
                          'digest' : formatHash(digest),
                          'is_config' : is_config})

    result = { 'format_version' : r['format_version'],
               'manifest' : file_list,
               'package_name' : r['package_name'],
               'package_version' : r['package_version'],
               'package_version_tuple' : r['package_version_tuple'],
               'timestamp' : formatTime(time.time())
             }

    for optKey in optKeys:
      if optKey in r:
        result[optKey] = r[optKey]

    THP_PACKAGE_SCHEMA.checkMatch(result)

    return result

def makeBundleObj(config_fname, getPackage, getPackageLength):
    """Given a description of a thandy  bundle in config_fname,
       return a new unsigned bundle object.  getPackage must be a function
       returning a package object for every package the bundle requires
       when given the package's name as input.  getPacakgeLength
       must be a function returning the length of the package file.
    """
    packages = []
    def ShortGloss(lang, val): packages[-1]['gloss'][lang] = val
    def LongGloss(lang, val): packages[-1]['longgloss'][lang] = val
    def Package(name, order, version=None, path=None, optional=False):
        packages.append({'name' : name,
                         'version' : version,
                         'path' : path,
                         'order' : order,
                         'optional' : optional,
                         'gloss' : {},
                         'longgloss' : {} })
    preload = { 'ShortGloss' : ShortGloss, 'LongGloss' : LongGloss,
                'Package' : Package }
    r = readConfigFile(config_fname,
                       ['name',
                        'os',
                        'version',
                        'location',
                        ], ['arch'], preload)

    result = { '_type' : "Bundle",
               'at' : formatTime(time.time()),
               'name' : r['name'],
               'os' : r['os'],
               'version' : r['version'],
               'location' : r['location'],
               'packages' : packages }
    if r.has_key('arch'):
        result['arch'] = r['arch']

    for p in packages:
        try:
            pkginfo = getPackage(p['name'])
        except KeyError:
            raise thandy.FormatException("No such package as %s"%p['name'])

        p['hash'] = formatHash(getDigest(pkginfo))
        p['length'] = getPackageLength(p['name'])
        if p['path'] == None:
            p['path'] = pkginfo['location']
        if p['version'] == None:
            p['version'] = pkginfo['version']

    BUNDLE_SCHEMA.checkMatch(result)
    return result

def versionIsNewer(v1, v2):
    """Return true if version v1 is newer than v2.  Both versions are
       given as lists of version components.
       >>> versionIsNewer([1,2,3], [1,2,3,4])
       False
       >>> versionIsNewer([1,2,3,5], [1,2,3,4])
       True
       >>> versionIsNewer([1,3,3,5], [1,2,3,5])
       True
    """
    return v1 > v2

def getBundleKey(bundlePath):
    """
       Return all parts of a bundle's "key" as used in a timestamp file,
       given its full filename.

       >>> getBundleKey("/bundleinfo/tor-browser/win32/some-file-name.txt")
       '/bundleinfo/tor-browser/win32/'
    """
    # No, we can't use "os.path.directory" or "os.path.split".  Those are
    # OD-dependent, and all of our paths are in Unix format.
    idx = bundlePath.rindex("/")
    return bundlePath[:idx+1]

def makeTimestampObj(mirrorlist_obj, mirrorlist_len,
                     keylist_obj, keylist_len,
                     bundle_objs):
    """Return a new unsigned timestamp object for a given set of inputs,
       where mirrorlist_obj and mirrorlist_len are a (signed, unencoded)
       mirror list, and its length on disk; keylist_obj and keylist_len
       are the same for the key list, and bundle_objs is a list of
       (object, length) tuples for all the bundles.
    """
    result = { '_type' : 'Timestamp',
               'at' : formatTime(time.time()) }
    result['m'] = [ mirrorlist_obj['ts'],
                    formatHash(getDigest(mirrorlist_obj)),
                    mirrorlist_len ]
    result['k'] = [ keylist_obj['ts'],
                    formatHash(getDigest(keylist_obj)),
                    keylist_len ]
    result['b'] = bundles = {}
    for bundle, bundleLen in bundle_objs:
        k = getBundleKey(bundle['location'])
        v = bundle['version']
        entry = [ v, bundle['location'], bundle['at'], formatHash(getDigest(bundle)), bundleLen ]
        if not bundles.has_key(k) or versionIsNewer(v, bundles[k][0]):
            bundles[k] = entry

    TIMESTAMP_SCHEMA.checkMatch(result)

    return result

class MirrorInfo:
    """A MirrorInfo holds the parsed value of a thandy mirror list's entry
       for a single mirror."""
    def __init__(self, name, urlbase, contents, weight):
        self._name = name
        self._urlbase = urlbase
        self._contents = contents
        self._weight = weight

    def canServeFile(self, fname):
        for c in self._contents:
            if rolePathMatches(c, fname):
                return True
        return False

    def getFileURL(self, fname):
        if self._urlbase[-1] == '/':
            return self._urlbase+fname
        else:
            return "%s/%s" % (self._urlbase, fname)

    def format(self):
        return { 'name' : self._name,
                 'urlbase' : self._urlbase,
                 'contents' : self._contents,
                 'weight' : self._weight }

def makeMirrorListObj(mirror_fname):
    """Return a new unsigned mirrorlist object for the mirrors described in
       'mirror_fname'.
    """
    mirrors = []
    def Mirror(*a, **kw): mirrors.append(MirrorInfo(*a, **kw))
    preload = {'Mirror' : Mirror}
    r = readConfigFile(mirror_fname, (), (), preload)
    result = { '_type' : "Mirrorlist",
               'ts' : formatTime(time.time()),
               'mirrors' : [ m.format() for m in mirrors ] }

    MIRRORLIST_SCHEMA.checkMatch(result)
    return result

def makeKeylistObj(keylist_fname, includePrivate=False):
    """Return a new unsigned keylist object for the keys described in
       'mirror_fname'.
    """
    keys = []
    def Key(obj): keys.append(obj)
    preload = {'Key': Key}
    r = readConfigFile(keylist_fname, (), (), preload)

    klist = []
    for k in keys:
        k = thandy.keys.RSAKey.fromJSon(k)
        if includePrivate and not k.isPrivateKey():
            raise thandy.FormatException("Private key information not found.")

        klist.append({'key': k.format(private=includePrivate), 'roles' : k.getRoles() })

    result = { '_type' : "Keylist",
               'ts' : formatTime(time.time()),
               'keys' : klist }

    KEYLIST_SCHEMA.checkMatch(result)
    return result

#XXXX could use taggedobj.  Defer till this has a unit test.
SCHEMAS_BY_TYPE = {
    'Keylist' : KEYLIST_SCHEMA,
    'Mirrorlist' : MIRRORLIST_SCHEMA,
    'Timestamp' : TIMESTAMP_SCHEMA,
    'Bundle' : BUNDLE_SCHEMA,
    'Package' : PACKAGE_SCHEMA,
    }

def checkSignedObj(obj, keydb=None):
    """Given a signed object, check whether it is well-formed and correctly
       signed with some key in keydb having the appropriate role.  On
       success, returns a SignatureStatus, the rule used to sign it,
       and the object's path in the repository.
    """

    SIGNED_SCHEMA.checkMatch(obj)
    try:
        tp = obj['signed']['_type']
    except KeyError:
        raise thandy.FormatException("Untyped object")
    try:
        schema = SCHEMAS_BY_TYPE[tp]
    except KeyError:
        raise thandy.FormatException("Unrecognized type %r" % tp)
    schema.checkMatch(obj['signed'])

    if tp == 'Keylist':
        role = "master"
        path = "/meta/keys.txt"
    elif tp == 'Mirrorlist':
        role = "mirrors"
        path = "/meta/mirrors.txt"
    elif tp == "Timestamp":
        role = 'timestamp'
        path = "/meta/timestamp.txt"
    elif tp == 'Bundle':
        role = 'bundle'
        path = obj['signed']['location']
    elif tp == 'Package':
        role = 'package'
        path = obj['signed']['location']
    else:
        raise ValueError("Unknown signed object type %r"%tp)

    ss = None
    if keydb is not None:
        ss = checkSignatures(obj, keydb, role, path)

    return ss, role, path
