Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

beebox / crossover   deb

Repository URL to install this package:

Version: 18.5.0-1 

/ opt / cxoffice / lib / python / c4parser.py

# (c) Copyright 2009-2012. CodeWeavers, Inc.

import os.path
import sys
import time
import traceback
import xml.sax
import xml.sax.handler

import cxproduct
import cxlog
import c4profiles

def install_sax_workaround():
    # work-around for a bug in xml.sax.xmlreader
    # see http://mail.python.org/pipermail/python-bugs-list/2006-December/036370.html
    def _self_contains(self, item):
        try:
            # pylint: disable=W0104
            self[item] # Yes, pylint, this really does have an effect.
            return True
        except KeyError:
            return False

    # pylint: disable=W0621
    import xml.sax.xmlreader

    if not hasattr(xml.sax.xmlreader.AttributesImpl, '__contains__'):
        xml.sax.xmlreader.AttributesImpl.__contains__ = _self_contains

install_sax_workaround()

class _C4PHandler(xml.sax.handler.ContentHandler):


    # This is the default for the property indicating which C4 profile to
    # install in the case of standalone c4p files.
    autorun = None

    def __init__(self, trusted, source, origin):
        xml.sax.handler.ContentHandler.__init__(self)
        self.revokelist = []
        self.disabled_licenses = []
        self.apps = []
        self.parse_errors = []
        self.locator = None

        self._objects = [self]
        self._states = [_C4PHandler.state_init]
        self._skip = 0

        self._key = None
        self._property = None
        self._string = None
        self._default = False

        self._trusted = trusted
        self._source = source
        self._origin = origin


    #####
    #
    # Standard SAX callbacks
    #
    #####

    def setDocumentLocator(self, locator):
        # pylint: disable=C0103
        self.locator = locator

    def startElement(self, name, attrs):
        """Called when the SAX parser finds an opening XML tag, <c4p> for
        instance.

        This calls the startElement handler which is at the top of the state
        stack. In turn this handler normally pushes a new state on top of the
        state stack to handle the tags contained therein.

        If the opening tag corresponds to an object, for instance <app>, then
        it will also push the nwe object on top of the object stack.
        """
        # pylint: disable=C0103
        self._states[-1][0](self, name, attrs)

    def endElement(self, name):
        """Called when the SAX parser finds a closing XML tag, </c4p> for
        instance.

        endElement() calls can always be exactly matched to a startElement()
        call. If the XML file is malformed, for instance if it has a mismatched
        closing tag, then the SAX parser will raise an exception rather than
        call endElement() on the mismatched tag.

        This calls the endElement handler which is at the top of the state
        stack. This handler must always pop the state stack, and should also
        pop the object stack if an object was created by the opening XML tag.
        """
        # pylint: disable=C0103
        self._states[-1][1](self, name)

    def characters(self, content):
        """Called by the SAX parser to handle the characters in between tags.

        This calls the characters handler which is at the top of the state
        stack. Note that these characters may be given by the SAX parser in
        arbitrarily small chunks. So it is usually necessary to concatenate
        them together.
        """
        self._states[-1][2](self, content)

    def endDocument(self):
        # pylint: disable=C0103
        if len(self._states) != 1:
            self.fail("an internal error occurred: len(_states) = %d" % len(self._states))


    #####
    #
    # Error reporting and debugging
    #
    #####

    def fail(self, error):
        """Reports a fatal error as a SAXParseException."""
        # SAXParseException really derives from Exception
        # pylint: disable=E0012,W0710
        raise xml.sax.SAXParseException(error, None, self.locator)

    def warn(self, message):
        """Prints a warning as a debug trace in the c4parser channel."""
        cxlog.log_("c4parser",
                   "%s:%d:%d: %s" % (self.locator.getSystemId(),
                                     self.locator.getLineNumber(),
                                     self.locator.getColumnNumber(), cxlog.to_str(message)))


    #####
    #
    # Some common handlers
    #
    #####

    def _startof_unexpected(self, name, _attrs):
        self.fail("unexpected opening tag %s" % name)

    def _end_simple(self, name):
        """Pops the state stack but leave the object stack as is.

        This is mostly used by attribute handlers.
        """
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
        self._states.pop()

    def _end_popobj(self, name):
        """Pops the state and object stacks."""
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
        self._objects.pop()
        self._states.pop()

    def _endof_unexpected(self, name):
        self.fail("unexpected closing tag %s" % name)

    def _unexpected_characters(self, content):
        """Warns about unexpected string snippets."""
        if not content.isspace():
            self.warn("unexpected characters '%s'" % repr(content))

    def _string_characters(self, content):
        """Collects the string snippets as SAX returns them so we can
        concatenate them later.
        """
        self._string.append(content)

    def _string_unexpected(self, _content):
        self.fail("unexpected string content")


    #####
    #
    # Properties represented by an object
    #
    # This parses a tag of the form '<tag><child>...</child></tag>' where
    # the <child> tag has already beed converted to an object which is to
    # be stored in the <current-object>.<prop> list.
    #
    #####

    def _startof_objectlist(self, prop, child, state):
        obj = self._objects[-1]
        if prop not in obj.__dict__:
            obj.__dict__[prop] = [child]
        else:
            obj.__dict__[prop].append(child)
        self._objects.append(child)
        self._states.append(state)


    #####
    #
    # Simple list of string tuples
    #
    # This parses a tag of the form
    #   <tag attr1="value1" attr2="value2" ...>string</tag>
    # and stores the specified child object in the current object's
    # childproc field, and the tag's attributes and string value as follows:
    #   <current-object>.<childprop> = <child>
    #   <child>.<attr1> = <value1>
    #   <child>.<attr2> = <value2>
    #   ...
    #   <child>.<prop>  = <string>
    #
    #####

    def _startof_tuplelist(self, childprop, child, attrs, prop):
        self._startof_objectlist(childprop, child, _C4PHandler.state_tuplelist)
        cxlog.log_('c4parser', "attrs=" + cxlog.debug_str(attrs))
        self._property = prop
        self._string = []
        for name, value in attrs.items():
            child.__dict__[name] = value
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))

    def _endof_tuplelist(self, _unused):
        obj = self._objects.pop()
        value = ''.join(self._string)
        obj.__dict__[self._property] = value
        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_tuplelist = (_startof_unexpected, _endof_tuplelist, _string_characters)


    #####
    #
    # Boolean properties
    #
    # This parses a tag of the form '<tag>boolean</tag>' and puts the boolean
    # in the <current-object>.<prop> field.
    #
    #####

    FALSE_SET = set(['false', '0', ''])

    def _startof_boolean(self, prop):
        self._property = prop
        self._string = []
        self._states.append(_C4PHandler.state_boolean)
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))

    def _endof_boolean(self, _unused):
        obj = self._objects[-1]
        value = ''.join(self._string)
        cxlog.log_('c4parser', "obj=%s property=%s value=%s" % (obj, cxlog.to_str(self._property), cxlog.debug_str(value)))
        obj.__dict__[self._property] = value not in _C4PHandler.FALSE_SET
        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_boolean = (_startof_unexpected, _endof_boolean, _string_characters)


    #####
    #
    # Integer properties
    #
    # This parses a tag of the form '<tag>integer</tag>' and puts the integer
    # in the <current-object>.<prop> field or warns if it has already been set.
    #
    #####

    def _startof_integer(self, prop):
        self._property = prop
        self._string = []
        self._states.append(_C4PHandler.state_integer)
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))

    def _endof_integer(self, _unused):
        obj = self._objects[-1]
        value = int(''.join(self._string), 10)
        cxlog.log_('c4parser', "obj=%s property=%s value=%s" % (cxlog.debug_str(obj), cxlog.to_str(self._property), cxlog.debug_str(value)))
        if self._property not in obj.__dict__:
            obj.__dict__[self._property] = value
        else:
            self.warn("%s has been set already" % self._property)

        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_integer = (_startof_unexpected, _endof_integer, _string_characters)


    #####
    #
    # String properties
    #
    # This parses a tag of the form '<tag>string</tag>' and puts the string
    # in the <current-object>.<prop> field or warns if it has already been set.
    #
    #####

    def _startof_string(self, prop):
        self._property = prop
        self._string = []
        self._states.append(_C4PHandler.state_string)
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))

    def _endof_string(self, _unused):
        if self._property is not None:
            obj = self._objects[-1]
            value = ''.join(self._string)
            cxlog.log_('c4parser', "obj=%s property=%s value=%s" % (cxlog.debug_str(obj), cxlog.to_str(self._property), cxlog.debug_str(value)))
            if self._property not in obj.__dict__:
                obj.__dict__[self._property] = value
            else:
                self.warn("%s has been set already" % self._property)

        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_string = (_startof_unexpected, _endof_string, _string_characters)


    #####
    #
    # String list properties
    #
    # This parses a tag of the form '<tag>string</tag>' and puts the string
    # in the <current-object>.<prop> list.
    #
    #####

    def _startof_stringlist(self, prop):
        self._property = prop
        self._string = []
        self._states.append(_C4PHandler.state_stringlist)
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))

    def _endof_stringlist(self, _unused):
        obj = self._objects[-1]
        value = ''.join(self._string)
        cxlog.log_('c4parser', "obj=%s property=%s value=%s" % (cxlog.debug_str(obj), cxlog.to_str(self._property), cxlog.debug_str(value)))
        if self._property not in obj.__dict__:
            obj.__dict__[self._property] = [value]
        else:
            obj.__dict__[self._property].append(value)
        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_stringlist = (_startof_unexpected, _endof_stringlist, _string_characters)


    #####
    #
    # String set properties
    #
    # This parses a tag of the form '<tag>string</tag>' and puts the string
    # in the <current-object>.<prop> set.
    #
    #####

    def _startof_stringset(self, prop):
        self._property = prop
        self._string = []
        self._states.append(_C4PHandler.state_stringset)
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))

    def _endof_stringset(self, _unused):
        obj = self._objects[-1]
        value = ''.join(self._string)
        cxlog.log_('c4parser', "obj=%s property=%s value=%s" % (cxlog.debug_str(obj), cxlog.to_str(self._property), cxlog.debug_str(value)))
        if self._property not in obj.__dict__:
            obj.__dict__[self._property] = set((value,))
        else:
            obj.__dict__[self._property].add(value)
        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_stringset = (_startof_unexpected, _endof_stringset, _string_characters)


    #####
    #
    # String map properties
    #
    # This parses a tag of the form '<tag attr="value">string</tag>', retrieves
    # the value of the attr attribute and stores the string as follows:
    #   <current-object>.<prop>[<value>] = <string>
    #
    #####

    def _startof_stringmap(self, prop, attr, attrs):
        self._property = prop
        self._key = attrs.get(attr, '')
        self._string = []
        self._states.append(_C4PHandler.state_stringmap)
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))

    def _endof_stringmap(self, _unused):
        obj = self._objects[-1]
        value = ''.join(self._string)
        cxlog.log_('c4parser', "obj=%s property=%s key=%s value=%s" % (cxlog.debug_str(obj), cxlog.to_str(self._property), cxlog.to_str(self._key), cxlog.debug_str(value)))
        if self._property not in obj.__dict__:
            obj.__dict__[self._property] = {self._key: value}
        else:
            obj.__dict__[self._property][self._key] = value
        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_stringmap = (_startof_unexpected, _endof_stringmap, _string_characters)


    #####
    #
    # Skipping tags
    #
    #####

    def _startof_skip(self, name, _attrs=None):
        if not self._skip:
            self.warn("skipping unknown tag %s" % name)
            self._states.append(_C4PHandler.state_skip)
        self._skip += 1

    def _endof_skip(self, _unused):
        self._skip -= 1
        if not self._skip:
            self._states.pop()

    def _ignore_characters(self, _content):
        pass

    state_skip = (_startof_skip, _endof_skip, _ignore_characters)


    #####
    #
    # Document handler
    #
    #####

    def _childof_init(self, name, _attrs):
        if name == 'c4p':
            self._states.append(_C4PHandler.state_c4p)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
        else:
            self.warn("expecting c4p tag, got %s" % name)

    state_init = (_childof_init, _endof_unexpected, _string_unexpected)


    #####
    #
    # <c4p> handler
    #
    #####

    def _childof_c4p(self, name, attrs):
        if name == 'revokelist':
            self._states.append(_C4PHandler.state_revokelist)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
        elif name == 'disabledlicenses':
            self._states.append(_C4PHandler.state_disabledlicenses)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
        elif name == 'applications':
            self._states.append(_C4PHandler.state_applications)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
        elif name == 'autorun':
            if 'appid' in attrs:
                self.autorun = attrs['appid']
            self._startof_skip(name)
        else:
            self._startof_skip(name)

    state_c4p = (_childof_c4p, _end_simple, _unexpected_characters)


    #####
    #
    # <revokelist> handler
    #
    #####

    def _childof_revokelist(self, name, attrs):
        if name == 'revokeapp':
            revokeapp = c4profiles.C4RevokeProfile()
            revokeapp.appid = attrs['appid']
            revokeapp.first = attrs.get('first', None)
            revokeapp.last = attrs.get('last', None)
            self.revokelist.append(revokeapp)
            self._states.append(_C4PHandler.state_revokeapp)
        else:
            self._startof_skip(name)

    state_revokelist = (_childof_revokelist, _end_simple, _unexpected_characters)


    #####
    #
    # <revokeapp> handler
    #
    #####

    state_revokeapp = (_startof_unexpected, _end_simple, _unexpected_characters)


    #####
    #
    # <disabledlicenses> handler
    #
    #####

    def _childof_disabledlicenses(self, name, _attrs):
        if name == 'id':
            self._startof_stringlist('disabled_licenses')
        else:
            self._startof_skip(name)

    state_disabledlicenses = (_childof_disabledlicenses, _end_simple, _unexpected_characters)


    #####
    #
    # <applications> handler
    #
    #####

    def _childof_applications(self, name, attrs):
        if name == 'app':
            app = c4profiles.C4Profile()
            app.appid = attrs['appid']
            app.timestamp = attrs.get('timestamp', None)
            app.trusted = self._trusted
            app.source = self._source
            app.origin = self._origin
            self.apps.append(app)
            self._objects.append(app)
            self._states.append(_C4PHandler.state_app)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
        else:
            self._startof_skip(name)

    state_applications = (_childof_applications, _end_simple, _unexpected_characters)

    def _endof_contributor(self, _unused):
        # pylint: disable=W0201
        obj = self._objects[-1]
        value = ''.join(self._string)
        cxlog.log_('c4parser', "obj=%s property=%s value=%s" % (cxlog.debug_str(obj), cxlog.to_str(self._property), cxlog.debug_str(value)))
        obj.name = value
        self._objects.pop()
        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_contributor = (_startof_unexpected, _endof_contributor, _string_characters)

    #####
    #
    # <app> handler
    #
    #####

    def _childof_app(self, name, attrs):
        if name == 'cxversion':
            self._startof_tuplelist('cxversions', c4profiles.C4CXVersion(),
                                    attrs, 'cxversion')
        elif name == 'name':
            self._startof_stringmap('localized_names', 'lang', attrs)

        elif name == 'contributor':
            # pylint: disable=W0201
            c4profile = self._objects[-1]
            contributor = c4profiles.C4CXContributor()
            if 'userid' in attrs and attrs['userid'] != '':
                contributor.userid = attrs['userid']
            c4profile = self._objects[-1]
            if 'contributor' in c4profile.__dict__:
                c4profile.contributor.append(contributor)
            else:
                c4profile.contributor = [contributor]
            self._string = []
            self._objects.append(contributor)
            self._states.append(_C4PHandler.state_contributor)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))

        elif name == 'appprofile':
            # pylint: disable=W0201
            c4profile = self._objects[-1]
            app_profile = c4profiles.C4ApplicationProfile()
            app_profile.parent = c4profile
            if c4profile.app_profile:
                self.fail("an application profile has already been declared")
            c4profile.app_profile = app_profile
            self._objects.append(app_profile)
            self._states.append(_C4PHandler.state_appprofile)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))

        elif name == 'installprofile':
            # pylint: disable=W0201
            c4profile = self._objects[-1]
            installer_profile = c4profiles.C4InstallerProfile()
            installer_profile.parent = c4profile
            if 'appid' in attrs and attrs['appid'] != '':
                installer_profile.appid = attrs['appid']
            c4profile = self._objects[-1]
            if 'installer_profiles' in c4profile.__dict__:
                c4profile.installer_profiles.append(installer_profile)
            else:
                c4profile.installer_profiles = [installer_profile]
            self._objects.append(installer_profile)
            self._states.append(_C4PHandler.state_installprofile)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))

        elif name == 'cdprofile':
            # pylint: disable=W0201
            c4profile = self._objects[-1]
            cd_profile = c4profiles.C4CDProfile()
            cd_profile.parent = c4profile
            if c4profile.cd_profile:
                self.fail("a cd profile has already been declared")
            c4profile.cd_profile = cd_profile
            self._objects.append(cd_profile)
            self._states.append(_C4PHandler.state_cdprofile)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))

        else:
            self._startof_skip(name)

    def _endof_app(self, _name):
        c4profile = self._objects.pop()
        try:
            c4profile.validate()
        except AttributeError:
            exception = sys.exc_info()[1]
            self.parse_errors.append(str(exception))
            cxlog.warn(cxlog.debug_str(exception))
            cxlog.warn("the %s profile contains errors, ignoring it" % cxlog.to_str(c4profile.appid))
            self.apps.pop()
            if cxlog.is_on('c4parser'):
                traceback.print_exc()
        except Exception: # pylint: disable=W0703
            exception = sys.exc_info()[1]
            msg = "an unexpected error occurred while reading the %s profile, ignoring it" % c4profile.appid
            self.parse_errors.append(msg + "\n" + str(exception))
            cxlog.warn(msg)
            traceback.print_exc()

        # To simplify debugging, also check that the object stack only contains
        # 'self' (for the autorun attribute) at this point
        if len(self._objects) != 1:
            self.fail("internal error: len(_objects) = %d" % len(self._objects))
        self._states.pop()

    state_app = (_childof_app, _endof_app, _unexpected_characters)

    def _endof_medal(self, _unused):
        # pylint: disable=W0201
        obj = self._objects[-1]
        value = ''.join(self._string)
        cxlog.log_('c4parser', "obj=%s property=%s value=%s" % (cxlog.debug_str(obj), cxlog.to_str(self._property), cxlog.debug_str(value)))
        obj.medal = value
        self._objects.pop()
        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))

    state_medal = (_startof_unexpected, _endof_medal, _string_characters)

    #####
    #
    # <appprofile> handler
    #
    #####

    def _childof_appprofile(self, name, attrs):
        if name == 'comment':
            # Just ignore it
            self._startof_string(None)
        elif name == 'description':
            self._startof_stringmap('_description', 'lang', attrs)
        elif name == 'medal':
            # pylint: disable=W0201
            c4profile = self._objects[-1]
            medal = c4profiles.C4CXMedal()
            medal.platform = attrs.get('platform', None)
            medal.version = attrs.get('version', None)
            medal.number = attrs.get('num', 0)
            medal.timestamp = attrs.get('last', None)
            medal.raw_rating = attrs.get('rating', None)
            c4profile = self._objects[-1]
            if 'medals' in c4profile.__dict__:
                c4profile.medals.append(medal)
            else:
                c4profile.medals = [medal]
            self._string = []
            self._objects.append(medal)
            self._states.append(_C4PHandler.state_medal)
            cxlog.log_('c4parser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
        elif name == 'category':
            self._startof_string('raw_category')
        elif name == 'flag':
            self._startof_stringset('flags')
        elif name == 'installerid':
            self._startof_string('installer_id')
        elif name == 'installedkeypattern':
            self._startof_string('installed_key_pattern')
        elif name == 'installeddisplaypattern':
            self._startof_string('installed_display_pattern')
        elif name == 'installedregistryglob':
            self._startof_objectlist('installed_registry_globs',
                                     c4profiles.C4RegistryGlob(),
                                     _C4PHandler.state_installedregistryglob)
        elif name == 'installedfileglob':
            self._startof_stringlist('installed_file_globs')
        elif name == 'bottletemplate':
            self._startof_bottletemplate(attrs)
        elif name == 'appbottlegroup':
            self._startof_string('application_group')
        elif name == 'downloadurl':
            self._startof_stringmap('download_urls', 'lang', attrs)
        elif name == 'downloadurl64':
            self._startof_stringmap('download_urls_64bit', 'lang', attrs)
        elif name == 'downloadpageurl':
            self._startof_stringmap('download_page_urls', 'lang', attrs)
        elif name == 'downloadglob':
            self._startof_stringlist('_download_globs')
        elif name == 'extrafor':
            self._startof_stringset('extra_fors')
        elif name == 'steamid':
            self._startof_string('steamid')
        else:
            self._startof_skip(name)

    state_appprofile = (_childof_appprofile, _end_popobj, _unexpected_characters)

    # <installedregistryglob> handler

    def _childof_installedregistryglob(self, name, _attrs):
        if name == 'key':
            self._startof_string('key_glob')
        elif name == 'value':
            self._startof_string('value_pattern')
        elif name == 'data':
            self._startof_string('data_pattern')
        else:
            self._startof_skip(name)

    state_installedregistryglob = (_childof_installedregistryglob, _end_popobj, _unexpected_characters)

    # <bottletemplate> handler

    def _startof_bottletemplate(self, attrs):
        self._string = []
        purpose = attrs.get('purpose', '')
        if not purpose or purpose == 'use':
            self._property = 'use'
        elif purpose == 'install':
            self._property = 'install'
        else:
            self._property = None
        self._states.append(_C4PHandler.state_bottletemplate)
        cxlog.log_('c4parser', "objects=%d states=%d" % (len(self._objects), len(self._states)))

    def _endof_bottletemplate(self, _unused):
        obj = self._objects[-1]
        value = ''.join(self._string)
        cxlog.log_('c4parser', "obj=%s property=bottle_types value=%s" % (cxlog.debug_str(obj), cxlog.debug_str(value)))
        if 'bottle_types' not in obj.__dict__:
            obj.__dict__['bottle_types'] = {'use': [], 'install': []}
        if self._property == 'use':
            obj.__dict__['bottle_types']['use'].append(value)
            obj.__dict__['bottle_types']['install'].append(value)
        elif self._property == 'install':
            obj.__dict__['bottle_types']['install'].append(value)
        self._states.pop()
        cxlog.log_('c4parser', "objects=%d states=%d bottle_types" % (len(self._objects), len(self._states)))

    state_bottletemplate = (_startof_unexpected, _endof_bottletemplate, _string_characters)


    #####
    #
    # <installprofile> handler
    #
    #####

    def _childof_installprofile(self, name, attrs):
        if name == 'useif':
            # pylint: disable=W0201
            obj = self._objects[-1]
            if 'use_if'  in obj.__dict__:
                self.fail('Duplicate <useif> tag')
            obj.use_if = c4profiles.C4ConditionUnary()
            self._objects.append(obj.use_if)
            self._states.append(self.state_condition)
        elif name == 'comment':
            # Just ignore it
            self._startof_string(None)
        elif name == 'installnotes':
            self._startof_stringmap('_installation_notes', 'lang', attrs)
        elif name == 'udfcheck':
            self._startof_string('udf_remount_check')
        elif name == 'localinstallerglob':
            self._startof_stringlist('local_installer_file_globs')
        elif name == 'predependency':
            self._startof_stringset('pre_dependencies')
        elif name == 'postdependency':
            self._startof_stringset('post_dependencies')
        elif name == 'cxdiagcheck':
            self._startof_stringlist('cxdiag_checks')
        elif name == 'installerglob':
            self._startof_stringlist('installer_file_globs')
        elif name == 'installertreatas':
            self._startof_string('installer_treatas')
        elif name == 'installeroption':
            self._startof_stringlist('installer_options')
        elif name == 'installersilentoption':
            self._startof_stringlist('installer_silent_options')
        elif name == 'installerwinver':
            self._startof_string('installer_winver')
        elif name == 'installerdlloverrides':
            self._startof_string('installer_dlloverrides')
        elif name == 'installerenvironment':
            self._startof_objectlist('installer_environment',
                                     c4profiles.C4EnvVar(),
                                     _C4PHandler.state_environment)
        elif name == 'selfextractthreshold':
            self._startof_integer('selfextract_threshold')
        elif name == 'selfextractoption':
            self._startof_stringlist('selfextract_options')
        elif name == 'selfextractsilentoption':
            self._startof_stringlist('selfextract_silent_options')
        elif name == 'zipencoding':
            self._startof_string('zip_encoding')
        elif name == 'filestocopy':
            self._startof_objectlist('files_to_copy',
                                     c4profiles.C4FilesToCopy(),
                                     _C4PHandler.state_filestocopy)
        elif name == 'postinstallreboot':
            self._startof_boolean('post_install_reboot')
        elif name == 'preinstallenvironment':
            self._startof_objectlist('pre_install_environment',
                                     c4profiles.C4EnvVar(),
                                     _C4PHandler.state_environment)
        elif name == 'preinstallregistry':
            self._startof_objectlist('pre_install_registry',
                                     c4profiles.C4Registry(),
                                     _C4PHandler.state_installregistry)
        elif name == 'prermfakedll':
            self._startof_stringlist('pre_rm_fake_dlls')
        elif name == 'preinstallnsplugindll':
            self._startof_stringlist('pre_install_nsplugin_dlls')
        elif name == 'ignorensplugindll':
            self._startof_stringlist('ignore_nsplugin_dlls')
        elif name == 'lnkfile':
            self._startof_objectlist('lnk_files',
                                     c4profiles.C4LnkFile(),
                                     _C4PHandler.state_lnkfile)
        elif name == 'skipmenu':
            self._startof_boolean('skip_menu_creation')
        elif name == 'skipassoc':
            self._startof_boolean('skip_assoc_creation')
        elif name == 'installnsplugins':
            self._startof_boolean('install_nsplugins')
        elif name == 'mainmenunever':
            self._startof_stringlist('mainmenu_never')
        elif name == 'defaulteassoc':
            self._startof_stringlist('default_eassocs')
        elif name == 'alteassoc':
            self._startof_stringlist('alt_eassocs')
        elif name == 'postregisterdll':
            self._startof_stringlist('post_registerdll')
        elif name == 'postinstallregistry':
            self._startof_objectlist('post_install_registry',
                                     c4profiles.C4Registry(),
                                     _C4PHandler.state_installregistry)
        elif name == 'postinstallurl':
            self._startof_stringmap('post_install_urls', 'lang', attrs)
        elif name == 'alwaysredownload':
            self._startof_boolean('always_redownload')
        else:
            self._startof_skip(name)

    state_installprofile = (_childof_installprofile, _end_popobj, _unexpected_characters)

    # condition handler (for <useif> & co)

    def _childof_condition(self, name, attrs):
        state = self.state_condition
        if name in c4profiles.C4ConditionCompare.OPERATORS:
            child = c4profiles.C4ConditionCompare(name)
            child.name = attrs['name']
            self._property = 'value'
            self._string = []
            state = self.state_equal
        elif name == 'match':
            # pylint: disable=R0204
            child = c4profiles.C4ConditionMatch()
            child.name = attrs['name']
            self._property = 'regexp'
            self._string = []
            state = self.state_match
        elif name == 'not':
            child = c4profiles.C4ConditionNot()
        elif name == 'and':
            child = c4profiles.C4ConditionAnd()
        elif name == 'or':
            child = c4profiles.C4ConditionOr()
        else:
            self._startof_unexpected(name, attrs)

        # pylint: disable=W0201
        parent = self._objects[-1]
        if isinstance(parent, c4profiles.C4ConditionNary):
            if 'children' not in parent.__dict__:
                parent.children = [child]
            else:
                parent.children.append(child)
        elif 'child' in parent.__dict__:
            self.fail("Too many children in a unary condition node")
        else:
            parent.child = child

        self._objects.append(child)
        self._states.append(state)

    state_condition = (_childof_condition, _end_popobj, _unexpected_characters)
    state_equal = (_startof_unexpected, _endof_tuplelist, _string_characters)
    state_match = (_startof_unexpected, _endof_tuplelist, _string_characters)


    # <environment> handler

    def _childof_environment(self, name, _attrs):
        if name == 'name':
            self._startof_string('name')
        elif name == 'value':
            self._startof_string('value')
        else:
            self._startof_skip(name)

    state_environment = (_childof_environment, _end_popobj, _unexpected_characters)


    # <filestocopy> handler

    def _childof_filestocopy(self, name, _attrs):
        if name == 'glob':
            self._startof_string('glob')
        elif name == 'dst':
            self._startof_string('destination')
        else:
            self._startof_skip(name)

    state_filestocopy = (_childof_filestocopy, _end_popobj, _unexpected_characters)


    # <preinstallregistry> and <postinstallregistry> handler

    def _childof_installregistry(self, name, _attrs):
        if name == 'key':
            self._startof_string('key')
        elif name == 'value':
            self._startof_objectlist('values', c4profiles.C4RegistryValue(),
                                     _C4PHandler.state_value)
        else:
            self._startof_skip(name)

    state_installregistry = (_childof_installregistry, _end_popobj, _unexpected_characters)


    # <value> handler

    def _childof_value(self, name, _attrs):
        if name == 'name':
            self._startof_string('name')
        elif name == 'data':
            self._startof_string('data')
        else:
            self._startof_skip(name)

    state_value = (_childof_value, _end_popobj, _unexpected_characters)


    # <lnkfile> handler

    def _childof_lnkfile(self, name, _attrs):
        if name == 'shortcut':
            self._startof_string('shortcut')
        elif name == 'target':
            self._startof_string('target')
        elif name == 'workdir':
            self._startof_string('workdir')
        else:
            self._startof_skip(name)

    state_lnkfile = (_childof_lnkfile, _end_popobj, _unexpected_characters)


    #####
    #
    # <cdprofile> handler
    #
    #####

    def _childof_cdprofile(self, name, _attrs):
        if name == 'comment':
            # Just ignore it
            self._startof_string(None)
        elif name == 'cdglob':
            self._startof_objectlist('globs', c4profiles.C4CDGlob(),
                                     _C4PHandler.state_cdglob)
        else:
            self._startof_skip(name)

    state_cdprofile = (_childof_cdprofile, _end_popobj, _unexpected_characters)


    # <cdglob> handler

    def _childof_cdglob(self, name, _attrs):
        if name == 'glob':
            self._startof_string('glob')
        elif name == 'pattern':
            self._startof_stringlist('patterns')
        else:
            self._startof_skip(name)

    state_cdglob = (_childof_cdglob, _end_popobj, _unexpected_characters)


#####
#
# The public API
#
#####


class C4PFile(object):
    """The in-memory representation of the contents of a c4p file."""

    def __init__(self, filename, trusted, source):
        self.mtime = 0
        self._filename = filename
        self._revokelist = None
        self._disabledlicenses = None
        self._profiles = None
        self._autorun = None
        self._trusted = trusted
        self._source = source
        self._updatedonce = False
        if source == c4profiles.dropin:
            # A dropin file might not persist, so read it in right now!
            self.update()

    def update(self):
        """Updates the C4PFile object if the file has changed since it was last
        read.

        Returns the mtime of file, or 0 if an error arises during parsing.
        """

        if self._updatedonce and self._source == c4profiles.dropin:
            # Dropin files aren't necessarily persistent, so we only
            # read them the first time through.
            return self.mtime
        try:
            mtime = os.path.getmtime(self._filename)
        except OSError:
            # If the file is missing, or not accessible, then just store None
            # as its mtime
            mtime = None
        if self.mtime == mtime:
            return self.mtime

        self._updatedonce = True
        start = time.time()
        if self._trusted:
            origin = "CodeWeavers"
        else:
            origin = os.path.basename(self._filename)
        handler = _C4PHandler(self._trusted, self._source, origin)

        try:
            xml.sax.parse(self._filename, handler)
        except:
            msg = "an unexpected error occurred while parsing '%s', ignoring the remainder of the file" % self._filename
            handler.parse_errors.append(msg)
            cxlog.warn(msg)
            traceback.print_exc()
            return 0

        self._revokelist = handler.revokelist
        self._disabledlicenses = handler.disabled_licenses
        self._profiles = handler.apps
        self._autorun = handler.autorun
        cxlog.log("parsing '%s' took %0.3fs" % (cxlog.to_str(self._filename), time.time() - start))

        self.mtime = mtime
        return self.mtime

    def get_autorun_id(self):
        """Returns the application id of the profile that should be selected
        when this file is loaded, or None. If no autorun profile is specified
        and this file contains profiles for only one appid, use that id."""
        autorun = self.autorun
        if not autorun:
            profiles = self.profiles
            # If all profiles have the same id, use that id
            if profiles:
                profile_id = profiles[0].appid
                for profile in profiles[1:]:
                    if profile.appid != profile_id:
                        break
                else:
                    autorun = profile_id
        return autorun

    def get_supported_product_ids(self):
        """Returns a set of product id strings of products that this file
        supports."""
        autorun = self.get_autorun_id()

        profiles = []
        # Use only autorun profiles, if there are any autorun profiles
        if autorun is not None:
            for profile in self.profiles:
                if profile.appid == autorun:
                    profiles.append(profile)
        if not profiles:
            profiles = self.profiles

        # Find the products supported by each appid
        supported_products = {}
        for profile in profiles:
            supported_by_profile = supported_products.setdefault(profile.appid, set())
            for cxversion in profile.cxversions:
                supported_by_profile.add(cxversion.product)

        union = set()
        for productlist in supported_products.values():
            for product in productlist:
                union.add(product)

        intersection = union.copy()
        for productlist in supported_products.values():
            intersection.intersection_update(productlist)

        if intersection:
            result = intersection
        else:
            result = union

        return result

    def _useful_for_current_product(self):
        # The 'cxoffice' product has subsumed 'cxgames'.
        return cxproduct.this_product()["productid"] in self.get_supported_product_ids() or \
                (cxproduct.this_product()["productid"] == 'cxoffice' and 'cxgames' in self.get_supported_product_ids())

    useful_for_current_product = property(_useful_for_current_product)

    def _version_number_to_tuple(self, version):
        result = []
        for i in version.split('.'):
            try:
                result.append(int(i))
            except ValueError:
                # not an integer value
                pass
        return tuple(result)

    def find_products(self):
        """Returns a list of dictionaries describing products that can install
        the autorun profile of this file.

        See cxproduct.enum_products() for the format of the dictionary."""
        result = []

        supported_products = self.get_supported_product_ids()

        all_products = cxproduct.enum_products()
        for product in all_products.itervalues():
            if product['productid'] in supported_products or \
               product['productid'] == 'cxoffice' and self._version_number_to_tuple(product['productversion']) >= (11,) and 'cxgames' in supported_products:
                result.append(product)

        return result

    def _getfilename(self):
        """Returns this CrossTie's filename."""
        return self._filename

    filename = property(_getfilename)

    def _gettrusted(self):
        """Returns True if this CrossTie file is trusted."""
        return self._trusted

    trusted = property(_gettrusted)

    def _getsource(self):
        """Returns this CrossTie's source category."""
        return self._source

    source = property(_getsource)

    def _getmalware_appid(self):
        """Returns an appid of a malicious profile, if this file is considered
        malicious overall."""
        autorun = self.get_autorun_id()

        any_accepted_autorun_profiles = False
        any_accepted_profiles = False
        any_revoked_profiles = False
        any_autorun_profiles = False
        revoked_appid = None

        if self.profiles:
            for profile in self.profiles:
                if profile.appid == autorun:
                    any_autorun_profiles = True
                if profile.is_revoked:
                    any_revoked_profiles = True
                    revoked_appid = profile.appid
                else:
                    any_accepted_profiles = True
                    if profile.appid == autorun:
                        any_accepted_autorun_profiles = True

        # A file is malware if all (and at least one) contained profiles are
        # revoked, or all (and at least one) contained autorun profiles are
        # revoked.
        if any_revoked_profiles:
            if any_autorun_profiles and not any_accepted_autorun_profiles:
                return autorun
            elif not any_accepted_profiles:
                return revoked_appid
        return None

    malware_appid = property(_getmalware_appid)

    def _getrevokelist(self):
        """Returns a list of the profile revocations contained in the c4p
        file.
        """
        self.update()
        return self._revokelist

    revokelist = property(_getrevokelist)

    def _getdisabledlicenses(self):
        """Returns a list of the disabled license id's contained in the c4p
        file.
        """
        self.update()
        return self._disabledlicenses

    disabled_licenses = property(_getdisabledlicenses)

    def _getprofiles(self):
        """Returns a list of the profiles contained in the c4p file."""
        self.update()
        return self._profiles

    profiles = property(_getprofiles)

    def _getautorun(self):
        """Returns the application id of the autorun profile if any, and None
        otherwise.
        """
        self.update()
        return self._autorun

    autorun = property(_getautorun)