# (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)