# (c) Copyright 2014-2015. CodeWeavers, Inc.
import sys
import time
import traceback
import xml.sax
import xml.sax.handler
import cxlog
import cxfixesobjs
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 _CXFixesHandler(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):
xml.sax.handler.ContentHandler.__init__(self)
self.release = None
self.distributions = {}
self.fixes = {}
self.parse_errors = []
self.locator = None
self._objects = [self]
self._states = [_CXFixesHandler.state_init]
self._skip = 0
self._key = None
self._property = None
self._string = None
self._default = False
#####
#
# 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 cxfixesparser channel."""
cxlog.log_("cxfixesparser",
"%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_('cxfixesparser', "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_('cxfixesparser', "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)
#####
#
# 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(_CXFixesHandler.state_string)
cxlog.log_('cxfixesparser', "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_('cxfixesparser', "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_('cxfixesparser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))
state_string = (_startof_unexpected, _endof_string, _string_characters)
#####
#
# Attribute string properties
#
# This parses a tag of the form '<tag attr="string"/>' and puts the string
# in the <current-object>.<prop> field.
#
#####
def _startof_attr2string(self, prop, attr, attrs):
obj = self._objects[-1]
value = attrs.get(attr, '')
if prop not in obj.__dict__:
obj.__dict__[prop] = value
else:
self.warn("%s has been set already" % prop)
self._states.append(_CXFixesHandler.state_attr2string)
cxlog.log_('cxfixesparser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))
state_attr2string = (_startof_unexpected, _end_simple, _string_unexpected)
#####
#
# 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(_CXFixesHandler.state_stringset)
cxlog.log_('cxfixesparser', "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_('cxfixesparser', "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_('cxfixesparser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(self._property)))
state_stringset = (_startof_unexpected, _endof_stringset, _string_characters)
#####
#
# Attribute string set properties
#
# This parses a tag of the form '<tag attr="string"/>' and puts the string
# in the <current-object>.<prop> set.
#
#####
def _startof_attr2stringset(self, prop, attr, attrs):
obj = self._objects[-1]
value = attrs.get(attr, '')
if prop not in obj.__dict__:
obj.__dict__[prop] = set((value,))
else:
obj.__dict__[prop].add(value)
self._states.append(_CXFixesHandler.state_attr2stringset)
cxlog.log_('cxfixesparser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(prop)))
state_attr2stringset = (_startof_unexpected, _end_simple, _string_unexpected)
#####
#
# Skipping tags
#
#####
def _startof_skip(self, name, _attrs=None):
if not self._skip:
self.warn("skipping unknown tag %s" % name)
self._states.append(_CXFixesHandler.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 == 'cxfixes':
self.release = int(attrs.get('release', '0'), 10)
self._states.append(_CXFixesHandler.state_cxfixes)
cxlog.log_('cxfixesparser', "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)
#####
#
# <cxfixes> handler
#
#####
def _childof_cxfixes(self, name, _attrs):
if name == 'distributions':
self._states.append(_CXFixesHandler.state_distributions)
cxlog.log_('cxfixesparser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
elif name == 'fixes':
self._states.append(_CXFixesHandler.state_fixes)
cxlog.log_('cxfixesparser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
else:
self._startof_skip(name)
state_cxfixes = (_childof_cxfixes, _end_simple, _unexpected_characters)
#####
#
# <distributions> handler
#
#####
def _childof_distributions(self, name, attrs):
if name == 'distribution':
priority = int(attrs.get('priority', '0'), 10)
distro = cxfixesobjs.CXDistribution(attrs['id'], attrs['name'],
priority)
self._objects.append(distro)
self._states.append(_CXFixesHandler.state_distribution)
cxlog.log_('cxfixesparser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
else:
self._startof_skip(name)
state_distributions = (_childof_distributions, _end_simple, _unexpected_characters)
#####
#
# <distribution> handler
#
#####
def _childof_distribution(self, name, attrs):
if name == 'glob':
distglob = cxfixesobjs.CXDistGlob(attrs['file'])
self._startof_objectlist('globs', distglob,
_CXFixesHandler.state_glob)
elif name == 'updatecmd':
self._startof_string('updatecmd')
elif name == 'packagecmd':
self._startof_string('packagecmd')
elif name == 'fallback':
self._startof_attr2string('fallback', 'distribution', attrs)
else:
self._startof_skip(name)
def _endof_distribution(self, _name):
distro = self._objects.pop()
try:
distro.validate()
self.distributions[distro.distid] = distro
except AttributeError:
exception = sys.exc_info()[1]
self.parse_errors.append(str(exception))
msg = "the %s distribution is not valid, ignoring it" % distro.distid
self.parse_errors.append(msg)
if cxlog.is_on('cxfixesparser'):
traceback.print_exc()
cxlog.warn(msg)
except: # pylint: disable=W0702
exception = sys.exc_info()[1] # Python 2 and 3 compatible
self.parse_errors.append(str(exception))
msg = "an unexpected error occurred while parsing the %s distribution, ignoring it" % distro.distid
self.parse_errors.append(msg)
if cxlog.is_on('cxfixesparser'):
traceback.print_exc()
cxlog.warn(msg)
# To simplify debugging, also check that the object stack only contains
# 'self' at this point
if len(self._objects) != 1:
self.fail("internal error: len(_objects) = %d" % len(self._objects))
self._states.pop()
state_distribution = (_childof_distribution, _endof_distribution, _unexpected_characters)
#####
#
# <glob> handler
#
#####
def _childof_glob(self, name, _attrs):
if name == 'pattern':
self._startof_stringset('patterns')
else:
self._startof_skip(name)
state_glob = (_childof_glob, _end_popobj, _unexpected_characters)
#####
#
# <fixes> handler
#
#####
def _childof_fixes(self, name, attrs):
if name == 'fix':
fix = cxfixesobjs.CXFix(attrs['id'], attrs.get('tiedto'))
self._objects.append(fix)
self._states.append(_CXFixesHandler.state_fix)
cxlog.log_('cxfixesparser', "objects=%d states=%d %s" % (len(self._objects), len(self._states), cxlog.to_str(name)))
else:
self._startof_skip(name)
state_fixes = (_childof_fixes, _end_simple, _unexpected_characters)
#####
#
# <fix> handler
#
#####
def _childof_fix(self, name, attrs):
if name == 'tiedto':
self._startof_attr2stringset('tiedto', 'id', attrs)
elif name == 'fixfor':
fix = self._objects[-1]
distfix = cxfixesobjs.CXDistFix(attrs['distribution'],
attrs.get('bitness', None))
# This is a CXDistFix, not a _CXFixesHandler as Pylint believes
# pylint: disable=E1101
fix.add_distfix(distfix)
self._objects.append(distfix)
self._states.append(_CXFixesHandler.state_fixfor)
else:
self._startof_skip(name)
def _endof_fix(self, _name):
fix = self._objects.pop()
try:
fix.validate()
self.fixes[fix.errid] = fix
except AttributeError:
exception = sys.exc_info()[1]
self.parse_errors.append(str(exception))
msg = "the %s fix is not valid, ignoring it" % fix.errid
self.parse_errors.append(msg)
if cxlog.is_on('cxfixesparser'):
traceback.print_exc()
cxlog.warn(msg)
except: # pylint: disable=W0702
exception = sys.exc_info()[1] # Python 2 and 3 compatible
self.parse_errors.append(str(exception))
msg = "an unexpected error occurred while parsing the %s fix, ignoring it" % fix.errid
self.parse_errors.append(msg)
if cxlog.is_on('cxfixesparser'):
traceback.print_exc()
cxlog.warn(msg)
# To simplify debugging, also check that the object stack only contains
# 'self' at this point
if len(self._objects) != 1:
self.fail("internal error: len(_objects) = %d" % len(self._objects))
self._states.pop()
state_fix = (_childof_fix, _endof_fix, _unexpected_characters)
#####
#
# <fixfor> handler
#
#####
def _childof_fixfor(self, name, attrs):
if name == 'package':
self._startof_attr2stringset('packages', 'name', attrs)
else:
self._startof_skip(name)
state_fixfor = (_childof_fixfor, _end_popobj, _unexpected_characters)
#####
#
# The public API
#
#####
def read_file(filename):
start = time.time()
handler = _CXFixesHandler()
try:
xml.sax.parse(filename, handler)
errors = cxfixesobjs.validate(handler.distributions, handler.fixes)
if not errors:
cxlog.log("parsing '%s' took %0.3fs" % (cxlog.to_str(filename), time.time() - start))
return (handler.release, handler.distributions, handler.fixes, handler.parse_errors)
handler.parse_errors.extend(errors)
except: # pylint: disable=W0702
exception = sys.exc_info()[1] # Python 2 and 3 compatible
import errno
if hasattr(exception, 'errno') and exception.errno == errno.ENOENT:
handler.parse_errors.append("'%s' does not exist" % filename)
else:
msg = "an unexpected error occurred while parsing '%s'. Ignoring this file" % filename
handler.parse_errors.append(msg)
if cxlog.is_on('cxfixesparser'):
traceback.print_exc()
cxlog.warn(msg)
return (None, None, None, handler.parse_errors)