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 / c4profiles.py

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

"""Defines classes to represent and manipulate the C4 profile information."""

import os
import re

import iso8601

import cxproduct
import cxconfig
import cxdecorators
import cxlog
import cxurlget
import cxutils
import distversion
# for localization
from cxutils import cxgettext as _

# A mapping of the language code to the plain English name for use by the GUI.
LANGUAGES = {
    '':      _('Default'),
    'af':    _('Afrikaans'),
    'ar':    _('Arabic'),
    'as':    _('Assamese'),
    'az':    _('Azerbaijani'),
    'be':    _('Belarusian'),
    'bg':    _('Bulgarian'),
    'bn':    _('Bengali'),
    'ca':    _('Catalan'),
    'cs':    _('Czech'),
    'da':    _('Danish'),
    'de':    _('German'),
    'el':    _('Greek'),
    'en':    _('English'),
    'en-au': _('English (Australia)'),
    'en-bz': _('English (Belize)'),
    'en-ca': _('English (Canada)'),
    'en-gb': _('English (Great Britain)'),
    'en-ie': _('English (Ireland)'),
    'en-jm': _('English (Jamaica)'),
    'en-nz': _('English (New Zealand)'),
    'en-ph': _('English (Philippines)'),
    'en-tt': _('English (Trinidad & Tobago)'),
    'en-us': _('English (USA)'),
    'en-za': _('English (South Africa)'),
    'en-zw': _('English (Zimbabwe)'),
    'es':    _('Spanish'),
    'es-mx': _('Spanish (Mexico)'),
    'et':    _('Estonian'),
    'eu':    _('Basque'),
    'fa':    _('Farsi'),
    'fi':    _('Finnish'),
    'fo':    _('Faroese'),
    'fr':    _('French'),
    'gd':    _('Gaelic'),
    'gu':    _('Gujarati'),
    'he':    _('Hebrew'),
    'hi':    _('Hindi'),
    'hr':    _('Croatian'),
    'hu':    _('Hungarian'),
    'hy':    _('Armenian'),
    'id':    _('Indonesian'),
    'is':    _('Icelandic'),
    'it':    _('Italian'),
    'ja':    _('Japanese'),
    'ka':    _('Georgian'),
    'kk':    _('Kazakh'),
    'kn':    _('Kannada'),
    'ko':    _('Korean'),
    'ky':    _('Kirghiz'),
    'lt':    _('Lithuanian'),
    'lv':    _('Latvian'),
    'mk':    _('Macedonian'),
    'ml':    _('Malayalam'),
    'mn':    _('Mongolian (Cyrillic)'),
    'mr':    _('Marathi'),
    'ms':    _('Malay (Malaysia)'),
    'mt':    _('Maltese'),
    'nb':    _(u'Norwegian (Bokm\u00e5l)'),
    'ne':    _('Nepali (India)'),
    'nl':    _('Dutch'),
    'no':    _('Norwegian'),
    'or':    _('Oriya'),
    'pa':    _('Punjabi'),
    'pl':    _('Polish'),
    # pt-pt
    'pt':    _('Portuguese (Portugal)'),
    'pt-br': _('Portuguese (Brazil)'),
    'rm':    _('Romansh'),
    'ro':    _('Romanian'),
    'ru':    _('Russian'),
    'sa':    _('Sanskrit'),
    'sb':    _('Sorbian'),
    'sk':    _('Slovak'),
    'sl':    _('Slovenian'),
    'sq':    _('Albanian'),
    'sr':    _('Serbian (Latin)'),
    'st':    _('Southern Sotho'),
    'sv':    _('Swedish'),
    'sw':    _('Swahili'),
    'syr':   _('Syriac'),
    'ta':    _('Tamil'),
    'te':    _('Telugu'),
    'th':    _('Thai'),
    'tn':    _('Tswana'),
    'tr':    _('Turkish'),
    'ts':    _('Tsonga'),
    'tt':    _('Tatar'),
    'uk':    _('Ukrainian'),
    'ur':    _('Urdu'),
    'uz':    _('Uzbek'),
    'vi':    _('Vietnamese'),
    'xh':    _('Xhosa'),
    'yi':    _('Yiddish'),
    'zh':    _('Chinese'),
    'zh-cn': _('Chinese (Simplified)'),
    'zh-hk': _('Chinese (Hong Kong SAR)'),
    'zh-sg': _('Chinese (Singapore)'),
    'zh-tw': _('Chinese (Traditional)'),
    'zu':    _('Zulu'),
    }


# This is the set of environment variables which are allowed in expandable
# profile strings. The values are given for documentation only.
ENVIRONMENT_VARIABLES = (
    'accessories',     # 'Programs\Accessories'
    'allusersprofile', # 'c:\users\Public'
    'appdata',         # 'c:\users\crossover\Application Data'
    'comspec',         # 'c:\windows\system32\cmd.exe'
    'desktop',         # 'c:\users\crossover\Desktop'
    'programfiles',    # 'c:\Program Files' or 'c:\Program Files (x86)'
    'programfiles64',  # 'c:\Program Files' or 'c:\Program Files (x86)'
    'programw6432',    # 'c:\Program Files' or ''
    'programs',        # 'Programs'
    'startmenu',       # 'c:\users\crossover\Start Menu'
    'systemdrive',     # 'c:'
    'systemroot',      # 'c:\windows'
    'temp',            # 'c:\windows\temp'
    'userprofile',     # 'c:\users\crossover'
    'windir',          # 'c\windows'
    'winsysdir',       # 'c:\windows\system32' or 'c:\windows\syswow64'
    'winsysdir64',     # 'c:\windows\system32'
    )


# keep this in sync with installtask.InstallTask._component_categories
_localized_categories_seq = {
    u'Component': (_('Non-Applications'), _('Components')),
    u'Component/Font': (_('Non-Applications'), _('Components'), _('Fonts')),
    u'Component/Fonts': (_('Non-Applications'), _('Components'), _('Fonts')),
    u'Component/Service Pack': (_('Non-Applications'), _('Components'), _('Service Packs')),
    u'Educational Software & CBT': (_('Educational'),),
    u'Educational': (_('Educational'),),
    u'Games': (_('Games'),),
    u'Games/1st Person Shooter': (_('Games'), _('First Person Shooter')),
    u'Games/Action Games': (_('Games'), _('Action Games')),
    u'Games/Adventures': (_('Games'), _('Adventure Games')),
    u'Games/Adventure Games': (_('Games'), _('Adventure Games')),
    u'Games/Adventures/Visual Novels': (_('Games'), _('Adventure Games'), _('Visual Novels')),
    u'Games/Adventure Games/Visual Novels': (_('Games'), _('Adventure Games'), _('Visual Novels')),
    u'Games/Casual Games': (_('Games'), _('Casual Games')),
    u'Games/DOS Games': (_('Games'), _('DOS Games')),
    u'Games/Educational games & children': (_('Games'), _('Educational Games')),
    u'Games/Educational Games': (_('Games'), _('Educational Games')),
    u'Games/Emulators': (_('Games'), _('Emulators')),
    u'Games/Fighting Games': (_('Games'), _('Fighting Games')),
    u'Games/First Person Shooter': (_('Games'), _('First Person Shooter')),
    u'Games/Fun Stuff': (_('Games'), _('Fun Stuff')),
    u'Games/Fun stuff': (_('Games'), _('Fun Stuff')),
    u'Games/Game Tools': (_('Games'), _('Game Tools')),
    u'Games/Online (MMORPG) Games': (_('Games'), _('Online Games')),
    u'Games/Online Games': (_('Games'), _('Online Games')),
    u'Games/Puzzle Games': (_('Games'), _('Puzzle Games')),
    u'Games/Racing Games': (_('Games'), _('Racing Games')),
    u'Games/Rhythm Games': (_('Games'), _('Rhythm Games')),
    u'Games/Role Playing Games': (_('Games'), _('Role Playing Games')),
    u'Games/Simulation Games': (_('Games'), _('Simulation Games')),
    u'Games/Sports Games': (_('Games'), _('Sports Games')),
    u'Games/Survival Games': (_('Games'), _('Survival Games')),
    u'Games/Strategy Games': (_('Games'), _('Strategy Games')),
    u'Multimedia': (_('Multimedia'),),
    u'Multimedia/Audio': (_('Multimedia'), _('Audio')),
    u'Multimedia/Audio/Audio Players': (_('Multimedia'), _('Audio'), _('Audio Players')),
    u'Multimedia/Audio/Music Composition': (_('Multimedia'), _('Audio'), _('Music Composition')),
    u'Multimedia/Audio/Sound Editing': (_('Multimedia'), _('Audio'), _('Sound Editing')),
    u'Multimedia/Graphics': (_('Multimedia'), _('Graphics')),
    u'Multimedia/Graphics/Animation, Rendering & 3D': (_('Multimedia'), _('Graphics'), _('Animation, Rendering & 3D')),
    u'Multimedia/Graphics/Graphics Editing': (_('Multimedia'), _('Graphics'), _('Photo Editing')),
    u'Multimedia/Graphics/Graphics Viewer': (_('Multimedia'), _('Graphics'), _('Graphics Viewer')),
    u'Multimedia/Graphics/Photo Editing': (_('Multimedia'), _('Graphics'), _('Photo Editing')),
    u'Multimedia/Media Players': (_('Multimedia'), _('Media Players')),
    u'Multimedia/Music Composition': (_('Multimedia'), _('Music Composition')),
    u'Multimedia/Photo Editing': (_('Multimedia'), _('Photo Editing')),
    u'Multimedia/Video': (_('Multimedia'), _('Video')),
    u'Multimedia/Video/Video Editing': (_('Multimedia'), _('Video'), _('Video Editing')),
    u'Networking & Communication': (_('Networking & Communication'),),
    u'Networking & Communication/Browsers': (_('Networking & Communication'), _('Browsers')),
    u'Networking & Communication/Chat, Instant Messaging & Telephony': (_('Networking & Communication'), _('Chat & Instant Messaging')),
    u'Networking & Communication/Chat & Instant Messaging': (_('Networking & Communication'), _('Chat & Instant Messaging')),
    u'Networking & Communication/Email, News & Groupware': (_('Networking & Communication'), _('Email & News Readers')),
    u'Networking & Communication/Email & News': (_('Networking & Communication'), _('Email & News Readers')),
    u'Networking & Communication/Email & News Readers': (_('Networking & Communication'), _('Email & News Readers')),
    u'Networking & Communication/File transfer & Sharing': (_('Networking & Communication'), _('File Transfer & Sharing')),
    u'Networking & Communication/File Transfer & Sharing': (_('Networking & Communication'), _('File Transfer & Sharing')),
    u'Networking & Communication/Net Tools': (_('Networking & Communication'), _('Net Tools')),
    u'Networking & Communication/Remote Access': (_('Networking & Communication'), _('Remote Access')),
    u'Non Applications': (_('Non-Applications'),),
    u'Non Applications/CrossTie Snippets': (_('Non-Applications'), _('CrossTie Snippets')),
    u'Non-Applications': (_('Non-Applications'),),
    u'Non-Applications/CrossTie Snippets': (_('Non-Applications'), _('CrossTie Snippets')),
    u'Non-Applications/Components': (_('Non-Applications'), _('Components')),
    u'Non-Applications/Components/Fonts': (_('Non-Applications'), _('Components'), _('Fonts')),
    u'Non-Applications/Components/Service Packs': (_('Non-Applications'), _('Components'), _('Service Packs')),
    u'Productivity': (_('Productivity'),),
    u'Productivity/Database': (_('Productivity'), _('Database')),
    u'Productivity/Defect Tracking & Help Desk': (_('Productivity'), _('Project Tracking & Help Desk')),
    u'Productivity/Desktop Publishing': (_('Productivity'), _('Desktop Publishing')),
    u'Productivity/Finance, Accounting & Project': (_('Productivity'), _('Finance & Accounting')),
    u'Productivity/Finance & Accounting': (_('Productivity'), _('Finance & Accounting')),
    u'Productivity/Project Tracking & Help Desk': (_('Productivity'), _('Project Tracking & Help Desk')),
    u'Productivity/Legal & law': (_('Productivity'), _('Legal & Law')),
    u'Productivity/Legal & Law': (_('Productivity'), _('Legal & Law')),
    u'Productivity/Office Suites': (_('Productivity'), _('Office Suites')),
    u'Productivity/Office Utilities': (_('Productivity'), _('Office Utilities')),
    u'Productivity/Presentation': (_('Productivity'), _('Spreadsheet & Presentation')),
    u'Productivity/Spreadsheet & Presentation': (_('Productivity'), _('Spreadsheet & Presentation')),
    u'Productivity/Spreadsheet': (_('Productivity'), _('Spreadsheet & Presentation')),
    u'Productivity/Text Editors': (_('Productivity'), _('Text Editors & Document Viewers')),
    u'Productivity/Text Editors & Document Viewers': (_('Productivity'), _('Text Editors & Document Viewers')),
    u'Productivity/Web Design': (_('Productivity'), _('Web Design')),
    u'Productivity/Word Processing': (_('Productivity'), _('Word Processing')),
    u'Programming & Software Engineering': (_('Programming & Development Tools'),),
    u'Programming & Development Tools': (_('Programming & Development Tools'),),
    u'Reference, Documentation & Info': (_('Reference, Documentation & Informational'),),
    u'Reference, Documentation & Informational': (_('Reference, Documentation & Informational'),),
    u'Reference, Documentation & Informational/Cooking & Nutrition': (_('Reference, Documentation & Informational'), _('Cooking & Nutrition')),
    u'Reference, Documentation & Informational/Religious Studies': (_('Reference, Documentation & Informational'), _('Religious Studies')),
    u'Reference, Documentation & Informational/Foreign Language & Speech': (_('Reference, Documentation & Informational'), _('Foreign Language & Speech')),
    u'Reference, Documentation & Informational/Genealogy': (_('Reference, Documentation & Informational'), _('Genealogy')),
    u'Scientific, Technical & Math': (_('Scientific, Technical & Math'),),
    u'Scientific, Technical & Math/Astronomy': (_('Scientific, Technical & Math'), _('Astronomy & Weather')),
    u'Scientific, Technical & Math/Astronomy & Weather': (_('Scientific, Technical & Math'), _('Astronomy & Weather')),
    u'Scientific, Technical & Math/Biology': (_('Scientific, Technical & Math'), _('Biology & Chemistry')),
    u'Scientific, Technical & Math/Biology & Chemistry': (_('Scientific, Technical & Math'), _('Biology & Chemistry')),
    u'Scientific, Technical & Math/CAD & CAE': (_('Scientific, Technical & Math'), _('CAD & EDA')),
    u'Scientific, Technical & Math/CAD & EDA': (_('Scientific, Technical & Math'), _('CAD & EDA')),
    u'Scientific, Technical & Math/Chemistry': (_('Scientific, Technical & Math'), _('Biology & Chemistry')),
    u'Scientific, Technical & Math/EDA': (_('Scientific, Technical & Math'), _('CAD & EDA')),
    u'Scientific, Technical & Math/Flowchart, Diagramming & Graphs': (_('Scientific, Technical & Math'), _('Flowchart, Diagramming & Graphs')),
    u'Scientific, Technical & Math/Mapping & GPS': (_('Scientific, Technical & Math'), _('Mapping & GPS')),
    u'Scientific, Technical & Math/Mathematics': (_('Scientific, Technical & Math'), _('Mathematics')),
    u'Special Purpose': (_('Special Purpose'),),
    u'Special Purpose/Crafting': (_('Special Purpose'), _('Crafting')),
    u'Special Purpose/Health & Medical': (_('Special Purpose'), _('Health & Medical')),
    u'Special Purpose/Device-Specific Interfaces': (_('Special Purpose'), _('Device-Specific Interfaces')),
    u'Unknown': (_('Unknown'),),
    u'Utilities': (_('Utilities'),),
    u'Utilities/Compression': (_('Utilities'), _('Compression & Conversion')),
    u'Utilities/Compression & Conversion': (_('Utilities'), _('Compression & Conversion')),
    u'Utilities/File System': (_('Utilities'), _('File System')),
    u'Utilities/Catalog & Inventory': (_('Utilities'), _('Catalog & Inventory')),
    u'Utilities/Disc Burning': (_('Utilities'), _('Disc Burning')),
    }

_localized_categories = dict((k, '/'.join(v)) for (k, v) in _localized_categories_seq.iteritems())

#####
#
# Helper functions for validating fields
#
#####

_RESTRICTED_GLOB_RE = re.compile(r'(?:^|/)\.[.?*]+(?:/|$)')

def _validate_file_globs(globs, location):
    """Check that the file globs don't try to escape the directory they are
    applied to.

    We do that by simply forbidding any path component that could match '..'.
    """
    # FIXME: If the string is expandable, then we could get stuff like
    #      '/.%Empty%./' that would be expanded to '/../'. It's expected that
    # the allowed environment variables won't start or end with a '.'. They
    # could end with a '/' or '\\' which could be exploited.
    for glob in globs:
        if _RESTRICTED_GLOB_RE.search(glob):
            raise AttributeError("invalid file glob '%s' in %s" % (glob, location))

def _validate_file_paths(paths, location):
    """Check that the file paths don't try to escape the directory they are
    applied to.
    """
    # FIXME: If the string is expandable, then we could get stuff like
    #      '/.%Empty%./' that would be expanded to '/../'
    for path in paths:
        # For now we simply apply the same rules as for globs since
        # '?' and '*' are not valid Windows file characters anyway.
        if _RESTRICTED_GLOB_RE.search(path):
            raise AttributeError("invalid file path '%s' in %s" % (path, location))

def _validate_regular_expression(pattern, location, allow_empty=True):
    if pattern:
        try:
            if re.match(pattern, '') and not allow_empty:
                raise AttributeError("the %s regular expression for %s is overly broad" % (pattern, location))
        except re.error:
            raise AttributeError("%s is an invalid regular expression for %s" % (pattern, location))

def _validate_language_field(field):
    # return True if there is at least one non-empty value
    for value in field.itervalues():
        if value:
            return True
    return False

_CXDIAG_CHECK_RE = re.compile(r'^(?:AppRequire:|AppRecommend:|Ignore:|Closed$)', re.IGNORECASE)

def _validate_cxdiag_checks(cxdiag_checks, location):
    """Check that the format of the cxdiag_check fields is correct.

    Note that this does not verify that the specified issue is known since
    old CrossOver versions should just ignore new issues.
    """
    for cxdiag_check in cxdiag_checks:
        if not _CXDIAG_CHECK_RE.search(cxdiag_check):
            raise AttributeError("invalid cxdiag_check '%s' in %s" % (cxdiag_check, location))
        _validate_regular_expression(cxdiag_check, location, False)



#####
#
# Helper functions for dumping the profiles
#
#####

def _dump_value(out, indent, value):
    if hasattr(value, 'dump'):
        out.write(" %s\n" % type(value))
        value.dump(out, indent + "| ")
    else:
        out.write(" %s\n" % repr(value))

def _dump_fields(out, indent, obj, fields):
    for field in fields:
        if field not in obj.__dict__:
            continue
        value = obj.__dict__[field]
        if isinstance(value, dict):
            for key in sorted(value.keys()):
                out.write("%s%s{%s} =" % (indent, field, key))
                _dump_value(out, indent, value[key])
        elif isinstance(value, list):
            for i in xrange(len(value)):
                out.write("%s%s[%d] =" % (indent, field, i))
                _dump_value(out, indent, value[i])
        else:
            out.write("%s%s =" % (indent, field))
            _dump_value(out, indent, value)



#####
#
# Helper functions for copying/updating C4 profile objects
#
#####

def _copy_value(value):
    try:
        copy_func = value.copy
    except AttributeError:
        # pylint: disable=C0123
        if type(value) is list:
            return list(value)
        return value
    else:
        return copy_func()

def _copy_values(attributes):
    for key, value in attributes.iteritems():
        if key != 'parent':
            attributes[key] = _copy_value(value)

def _update_value(this, oth):
    try:
        update_func = this.update
    except AttributeError:
        # pylint: disable=C0123
        if type(this) is list:
            this.extend(oth)
            return this
        return oth
    else:
        result = update_func(oth)
        if result is None:
            return this
        return result

def _update_values(attributes, others, exclude=(), special=()):
    for key, value in others.iteritems():
        if key in exclude:
            continue
        if key in attributes:
            # pylint: disable=C0123
            if key in special:
                attributes[key] = special[key](attributes[key], value)
            elif type(value) in special:
                attributes[key] = special[type(value)](attributes[key], value)
            else:
                attributes[key] = _update_value(attributes[key], value)
        else:
            attributes[key] = _copy_value(value)

def _update_html(this, oth):
    return '%s<br/>%s' % (this, oth)

def _update_html_dict(this, oth):
    _update_values(this, oth, special={cxutils.unicode_type: _update_html})
    return this


#####
#
# C4Condition
#
#####

class C4Condition(object):
    """Represents a boolean expression."""

    @cxdecorators.abstractmethod
    def is_true(self, _properties):
        raise NotImplementedError()


class C4ConditionUnary(C4Condition):
    """A condition node with a single sub-node."""

    # A node to use to compute this condition. This field is mandatory and
    # thus defaults to None.
    child = None

    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if 'child' not in self.__dict__:
            raise AttributeError("C4ConditionUnary.child is not set for application %s" % app_name)
        self.child.validate(app_name)

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('child',))


    ### Condition evaluation

    def is_true(self, properties):
        return self.child.is_true(properties)


class C4ConditionNot(C4ConditionUnary):
    """Inverts the value of the child condition node."""

    ### Condition evaluation

    def is_true(self, properties):
        return not self.child.is_true(properties)


class C4ConditionNary(C4Condition):
    """A condition node that merges the result of multiple condition subnodes.
    """

    # A list of nodes node to use to compute this condition. This field is
    # mandatory and thus defaults to [].
    children = []

    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if not self.children:
            raise AttributeError("C4ConditionNary.children is empty for application %s" % app_name)
        for child in self.children:
            child.validate(app_name)

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('children',))

    @cxdecorators.abstractmethod
    def is_true(self, properties):
        # so pylint recognizes this as an abstract class
        raise NotImplementedError()


class C4ConditionAnd(C4ConditionNary):
    """Returns True if all the children nodes evaluate to True."""

    def is_true(self, properties):
        for child in self.children:
            if not child.is_true(properties):
                return False
        return True


class C4ConditionOr(C4ConditionNary):
    """Returns False if all the children nodes evaluate to False."""

    def is_true(self, properties):
        for child in self.children:
            if child.is_true(properties):
                return True
        return False


class C4ConditionCompare(C4Condition):
    """Returns True if the specified property has the expected value."""

    OPERATORS = frozenset(('equal', 'lt', 'le', 'ge', 'gt'))

    # The property name. This field is mandatory and thus defaults to None.
    name = None

    # The property's expected value.
    # This field is mandatory and thus defaults to None.
    value = None

    def __init__(self, operator):
        C4Condition.__init__(self)
        if operator not in C4ConditionCompare.OPERATORS:
            raise AttributeError("unknown operator %s" % operator)
        self.operator = operator

    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if 'name' not in self.__dict__:
            raise AttributeError("C4ConditionCompare.name is not set for application %s" % app_name)
        if 'value' not in self.__dict__:
            raise AttributeError("C4ConditionCompare.value is not set for application %s" % app_name)

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('operator', 'name', 'value'))


    ### Condition evaluation

    def is_true(self, properties):
        if self.operator == 'equal':
            return properties.get(self.name, "") == self.value
        if self.operator == 'lt':
            return properties.get(self.name, "") < self.value
        if self.operator == 'le':
            return properties.get(self.name, "") <= self.value
        if self.operator == 'ge':
            return properties.get(self.name, "") >= self.value
        return properties.get(self.name, "") > self.value


class C4ConditionMatch(C4Condition):
    """Returns True if the specified property matches the given regular expression."""

    # The property name. This field is mandatory and thus defaults to None.
    name = None

    # The regular expression that the property should match.
    # This field is mandatory and thus defaults to None.
    regexp = None

    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if 'name' not in self.__dict__:
            raise AttributeError("C4ConditionMatch.name is not set for application %s" % app_name)
        if 'regexp' not in self.__dict__:
            raise AttributeError("C4ConditionMatch.regexp is not set for application %s" % app_name)

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('name', 'regexp'))


    ### Condition evaluation

    def is_true(self, properties):
        try:
            return re.match(self.regexp, properties.get(self.name, ""))
        except re.error:
            cxlog.warn("useif match pattern %s for name %s is an invalid regular expression" % (cxlog.debug_str(self.regexp), cxlog.debug_str(self.name)))
            return None



#####
#
# C4CDProfile class
#
#####

class C4CDGlob(object):
    """Contains a specific glob for detecting an application CD."""

    ### Property defaults

    # A string containing a glob to identify the files to analyze. This field
    # is mandatory and thus defaults to none.
    glob = None

    # A list of regular expressions that the selected file must all match.
    # If the list is empty, then it's enough for the file to just exist.
    patterns = tuple()


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if 'glob' not in self.__dict__:
            raise AttributeError("C4CDGlob.glob is not set for application %s" % app_name)
        _validate_file_globs((self.glob,), "the C4CDGlob.glob field of %s" % app_name)
        for pattern in self.patterns:
            _validate_regular_expression(pattern, "the C4CDGlob.patterns field of %s" % app_name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('glob', 'patterns'))


class C4CDProfile(object):
    """Contains the C4 CD Profile information."""

    ### Property defaults

    # Points to the umbrella C4Profile object that ties a C4 entry's
    # specialized profiles together. This field is mandatory and thus defaults
    # to none.
    parent = None

    # A list of C4CDGlobs to be used to recognize a given CD. This field is
    # mandatory and thus defaults to None.
    globs = None


    def copy(self):
        result = C4CDProfile()
        result.__dict__.update(self.__dict__)
        _copy_values(result.__dict__)
        return result

    def update(self, oth):
        _update_values(self.__dict__, oth.__dict__, exclude=('parent',))


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if not self.parent:
            raise AttributeError("C4CDProfile.parent is not set for application %s" % app_name)
        if not self.globs:
            raise AttributeError("C4CDProfile.globs is not set for application %s" % app_name)
        # pylint: disable=E1133
        for glob in self.globs:
            glob.validate(app_name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('globs',))



#####
#
# C4ApplicationProfile class
#
#####


class C4RegistryGlob(object):
    """Contains a specific registry glob for detecting an installed
    application.
    """

    ### Property defaults

    # A string containing a glob to identify the registry keys to analyze.
    # This field is mandatory and thus has no default.
    # key_glob = ""

    # A string containing a regular expression to identify an optional key
    # value to analyze. If left empty, then the values are ignored. Note that
    # a key's default value is called '@'.
    value_pattern = ""

    # A string containing a regular expression to check if the value contains
    # the expected data. If left empty, then the presence of the value is
    # enough.
    data_pattern = ""


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if 'key_glob' not in self.__dict__:
            raise AttributeError("C4RegistryGlob.key_glob is not set for application %s" % app_name)
        _validate_regular_expression(self.value_pattern, "the C4RegistryGlob.value_pattern field of %s" % app_name)
        if self.data_pattern != "":
            if self.value_pattern == "":
                raise AttributeError("C4RegistryGlob.data_pattern is set but not the corresponding value_pattern for application %s" % app_name)
            _validate_regular_expression(self.data_pattern, "the C4RegistryGlob.data_pattern field of %s" % app_name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self,
                     ('key_glob', 'value_pattern', 'data_pattern'))


class C4ApplicationProfile(object):
    """Contains the C4 Application Profile information."""

    ### Enumerated types

    # A set of all known medals
    ALL_MEDALS = set(('gold', 'silver', 'bronze',
                      'ungold', 'unsilver', 'unbronze',
                      'knownnottowork', 'untested', 'untrusted'))

    # A list of the medals for supported applications.
    # Note that the supported and unsupported medal lists
    # are ordered and their values correspond. This is needed
    # for when we need to programmatically promote or demote an app.
    SUPPORTED_MEDALS = ['gold', 'silver', 'bronze']

    # A list of the medals for supported applications. See note
    # about ordering above.
    UNSUPPORTED_MEDALS = ['ungold', 'unsilver', 'unbronze']

    # A set of medals known to work
    BRONZE_PLUS_MEDALS = set(('gold', 'silver', 'bronze',
                              'ungold', 'unsilver', 'unbronze'))

    UNTRUSTED_MEDAL = 'untrusted'

    ### Property defaults

    # Points to the umbrella C4Profile object that ties a C4 entry's
    # specialized profiles together. This field is mandatory and thus defaults
    # to None.
    parent = None

    # A dictionary mapping the language to the application description. The
    # special '' mapping corresponds to the default application description.
    _description = {'': ''}

    _medalcache = "empty"

    # The C4 application category string. This field is mandatory and thus
    # defaults to None.
    raw_category = None

    _category = None

    # A list of C4CXMedal objects representing the times and situations this
    # application has been ranked for.
    medals = tuple()

    # A set of flag strings.
    flags = set()

    # If we know how to install this application, then this is the id of the
    # C4 entry that has the installation information.
    installer_id = None

    # If this regular expression matches an Uninstall key, then it means this
    # application is installed.
    installed_key_pattern = None

    # If this regular expression matches an Uninstall key's display name,
    # then it means this application is installed.
    installed_display_pattern = None

    # A list of C4RegistryGlobs to look for to check if the application is
    # installed.
    installed_registry_globs = tuple()

    # A list of strings containing the file globs to look for to check if the
    # application is installed.
    installed_file_globs = tuple()

    # The list of bottle types the application can be installed in.
    # The first entry is the default one.
    bottle_types = {'use': ("winxp",), 'install': ()}

    # Used to group related applications in the same bottle.
    application_group = ""

    # Maps a language to the URL where one can get the corresponding
    # installer.
    download_urls = {}

    # Overrides download_urls when the bottle is 64-bit.
    download_urls_64bit = {}

    # Maps a language to the URL of a web page from which the installer can
    # be manually downloaded.
    download_page_urls = {}

    # A list of strings containing the file globs to look for an installer file
    # that the user may have manually downloaded.
    _download_globs = tuple()

    # If the profile did not specify download globs we will try to derive
    # them from the download URLs and the result will be stored here.
    _auto_download_globs = None

    # A set of application ids that this is an upgrade or add-on for. Note
    # that this is not a dependency but a recommendation system. This can be
    # used for the following scenarios:
    # - A component which is an upgrade for another component.
    # - A recommended 'upgrade' which is a full install and thus does not
    #   depend on the item it upgrades.
    # - A codec / plugin / add-on which we recommend installing, but which
    #   technically is not an upgrade.
    extra_fors = set()

    # Optional steam id. If this is set then an app can be installed
    # via Steam.
    steamid = None



    def copy(self):
        result = C4ApplicationProfile()
        result.__dict__.update(self.__dict__)
        _copy_values(result.__dict__)
        return result

    def update(self, oth):
        _update_values(self.__dict__, oth.__dict__, exclude=('parent',))


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set on this profile
        and in the connected profiles.
        """
        if not self.parent:
            raise AttributeError("C4ApplicationProfile.parent is not set for application %s" % app_name)
        if not _validate_language_field(self._description):
            raise AttributeError("C4ApplicationProfile has no default description for application %s" % app_name)
        if not self.category:
            raise AttributeError("C4ApplicationProfile.category is not set for application %s" % app_name)
        check_template = False
        for medal in self.medals:
            medal.validate(app_name)
            # parent is always a C4Profile object which has a trusted attribute
            # pylint: disable=E1101
            if not self.parent.trusted:
                medal.medal = self.UNTRUSTED_MEDAL
            if medal.medal in self.BRONZE_PLUS_MEDALS:
                check_template = True
        if check_template and ('bottle_types' not in self.__dict__ or \
                                   not self.bottle_types['use']):
            cxlog.warn('%s has a bronze or higher medal so it should specify the bottle template(s) to use' % cxlog.to_str(app_name))
        _validate_regular_expression(self.installed_key_pattern, "the C4ApplicationProfile.installed_key_pattern field of %s" % app_name, False)
        _validate_regular_expression(self.installed_display_pattern, "the C4ApplicationProfile.installed_display_pattern field of %s" % app_name, False)
        for registryglob in self.installed_registry_globs:
            registryglob.validate(app_name)
        _validate_file_globs(self.installed_file_globs, "the installed_file_globs field of %s" % cxlog.to_str(app_name))
        _validate_file_globs(self._download_globs, "the download_globs field of %s" % cxlog.to_str(app_name))

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self,
                     ('_description', 'category', 'medals', 'flags',
                      'installer_id', 'installed_key_pattern',
                      'installed_display_pattern', 'installed_registry_globs',
                      'installed_file_globs', 'bottle_types',
                      'application_group', 'download_urls', 'download_urls_64bit',
                      'download_page_urls', '_download_globs', 'extra_fors'
                     ))


    ### Accessors for special properties

    def _getdescription(self):
        """Returns the application description in the current language.

        If no description is available for the current language, then the
        default description is returned.
        """
        _lang, description = cxutils.get_language_value(self._description)
        return description

    description = property(_getdescription)


    def _getmedal(self):
        """Returns the application medal for the current platform."""
        if self._medalcache != "empty":
            return self._medalcache

        defaultmedal = None
        product_version = cxutils.version_as_tuple()
        score = (-1, )
        for medal in self.medals:
            if medal.platform == distversion.PLATFORM:
                if medal.version:
                    medal_version = cxutils.split_version(medal.version)
                    if medal_version == product_version:
                        defaultmedal = medal
                        break
                    if medal_version < product_version:
                        new_score = medal_version
                    else:
                        continue
                else:
                    new_score = (0, )
                if defaultmedal is None or new_score > score:
                    defaultmedal = medal
                    score = new_score

        if defaultmedal:
            if defaultmedal.medal not in C4ApplicationProfile.SUPPORTED_MEDALS and defaultmedal.medal not in C4ApplicationProfile.UNSUPPORTED_MEDALS:
                self._medalcache = defaultmedal
                return defaultmedal

            if not supported_list.ids():
                # No supported list, which means we don't need to mess
                #  with anything.
                self._medalcache = defaultmedal
                return defaultmedal

            # parent is always a C4Profile object which has an appid attribute
            # pylint: disable=E1101
            if defaultmedal.medal in C4ApplicationProfile.SUPPORTED_MEDALS and self.parent.appid not in supported_list.ids():
                # We need to de-support this.
                defaultmedal.medal = C4ApplicationProfile.UNSUPPORTED_MEDALS[C4ApplicationProfile.SUPPORTED_MEDALS.index(defaultmedal.medal)]

            if defaultmedal.medal in C4ApplicationProfile.UNSUPPORTED_MEDALS and self.parent.appid in supported_list.ids():
                # We need to support this.
                defaultmedal.medal = C4ApplicationProfile.SUPPORTED_MEDALS[C4ApplicationProfile.UNSUPPORTED_MEDALS.index(defaultmedal.medal)]

            self._medalcache = defaultmedal
            return defaultmedal

        self._medalcache = None
        return None

    def _getmedal_rank(self):
        medal = self._getmedal()
        if medal:
            return medal.medal
        return 'untested'

    medal = property(_getmedal_rank)

    def _getmedal_icon(self):
        """Returns the icon name for this application's medal."""
        return os.path.join(cxutils.CX_ROOT,
                            'share/images/medal_' + self._getmedal_rank() + '.png')

    medal_icon = property(_getmedal_icon)

    def _getmedal_date(self):
        medal = self._getmedal()
        if medal:
            return medal.timestamp
        return ''

    medal_date = property(_getmedal_date)

    def _getmedal_rating(self):
        # pylint: disable=E1101
        if not self.parent.trusted:
            return -1 # untrusted
        medal = self._getmedal()
        if medal:
            return medal.rating
        return 0 # untested

    medal_rating = property(_getmedal_rating)

    def _getmedal_version(self):
        # pylint: disable=E1101
        medal = self._getmedal()
        if medal:
            return medal.version
        return None

    medal_version = property(_getmedal_version)

    def _getmedal_count(self):
        # pylint: disable=E1101
        medal = self._getmedal()
        if medal:
            return medal.number
        return 0

    medal_count = property(_getmedal_count)

    def _getis_supported(self):
        """Returns true if the application is supported."""
        try:
            # parent is always a C4Profile object which has a trusted attribute
            # pylint: disable=E1101
            return self.parent.trusted and self.medal in C4ApplicationProfile.SUPPORTED_MEDALS
        except KeyError:
            return False

    is_supported = property(_getis_supported)

    def _getis_virtual(self):
        return "virtual" in self.flags

    is_virtual = property(_getis_virtual)


    def _getpreferred_bottle_type(self):
        """Returns the profile's preferred bottle type."""
        if self.bottle_types and self.bottle_types['use']:
            return self.bottle_types['use'][0]
        return None

    preferred_bottle_type = property(_getpreferred_bottle_type)

    # Regular expressions for _getdownload_globs()
    # Cases to take into account when generalizing versions:
    # Foo-1.2.3.exe Foo_1_2_3.exe FooW32_1.2.3.exe Foo_1.2_b113.exe
    # Foo0.1.2.zip Foo-0.97a.exe foo-1.2.3-bar-4.5.6-win32.exe
    _GLOB_VERSION_RE = re.compile(r'[^0-9](?!32_)[1-9][0-9]*(?P<subver>(?:[_.][ab]?[0-9]+(?:[ab](?=[._-]))?)+)[^0-9]')
    # Cases to take into account when removing the 'demo' tag:
    # FooDemo.exe Foo-1.5demo.exe Demo1.exe
    _GLOB_DEMO_RE = re.compile(r'\*?[.-_]?(?:demo|downloader|full)(?=[0-9._-])', re.IGNORECASE)

    def _getdownload_globs(self):
        if self._download_globs:
            return self._download_globs
        if self._auto_download_globs is not None:
            return self._auto_download_globs

        self._auto_download_globs = []
        if not self.download_urls or self.download_urls_64bit:
            return self._auto_download_globs

        # Try to derive the download globs from the download URLs.
        known_exts = set(('.bat', '.cab', '.cmd', '.exe', '.msi', '.msp',
                          '.otf', '.rar', '.ttc', '.ttf', '.tgz', '.zip'))
        globs = set(('', 'install.exe', 'install.msi', 'installer.exe',
                     'installer.msi', 'setup.exe', 'setup.msi'))
        for _locale, url in self.download_urls.iteritems():
            glob = cxurlget.url_to_basename(url)
            if not cxutils.is_valid_windows_filename(glob):
                continue
            lower = glob.lower()
            if lower in globs:
                continue
            ext = (os.path.splitext(lower))[1]
            if ext in known_exts:
                newglob = ''
                pos = 0
                for match in C4ApplicationProfile._GLOB_VERSION_RE.finditer(glob):
                    newglob += glob[pos:match.start('subver') + 1] + '*'
                    pos = match.end('subver')
                newglob += glob[pos:]
                newglob = C4ApplicationProfile._GLOB_DEMO_RE.sub('*', newglob, 1)
                if len(newglob) < 7:
                    # This is too short and likely looks like 's*.exe'
                    self._auto_download_globs.append(glob)
                else:
                    self._auto_download_globs.append(newglob)
                globs.add(lower)

        return self._auto_download_globs

    download_globs = property(_getdownload_globs)

    def _getis_trusted_source(self):
        # parent is always a C4Profile object which has a trusted attribute
        # pylint: disable=E1101
        return self.parent.trusted

    is_trusted_source = property(_getis_trusted_source)

    def _get_origin(self):
        # parent is always a C4Profile object which has an origin attribute
        # pylint: disable=E1101
        return self.parent.origin

    origin = property(_get_origin)

    def _get_category(self):
        if self._category is None:
            category = _localized_categories.get(self.raw_category)
            if category is None:
                cxlog.warn('category %s has no localized category name' % cxlog.to_str(self.raw_category))
                _localized_categories[self.raw_category] = self._category = self.raw_category
            else:
                self._category = category
        return self._category

    category = property(_get_category)

#####
#
# C4InstallerProfile class
#
#####

UNIX_ENVIRONMENT_BLACKLIST = frozenset(('IFS', 'LD_LIBRARY_PATH', 'PATH'))

class C4EnvVar(object):
    """Describes an environment variable to set."""

    ### Property defaults

    # The name of the environment variable to set.
    # This field is mandatory and thus has no default.
    # name = ""

    # The value of the environment variable to set.
    # This field is mandatory and thus has no default.
    # value = ""


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if 'name' not in self.__dict__:
            raise AttributeError("C4Environment.name is not set for application %s" % app_name)
        elif self.__dict__["name"] in UNIX_ENVIRONMENT_BLACKLIST:
            raise AttributeError("Modifying the '%s' Unix environment variable is not allowed for application %s" % (self.__dict__["name"], app_name))
        if 'value' not in self.__dict__:
            raise AttributeError("C4Environment.value is not set for application %s" % app_name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('name', 'value'))


class C4FilesToCopy(object):
    """Specifies a set of files to copy."""

    ### Property defaults

    # A glob identifying the files to copy.
    # This field is mandatory and thus defaults to None.
    glob = None

    # A restricted path specifying where to copy the above files.
    # This field is mandatory and thus defaults to None.
    destination = None


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if 'glob' not in self.__dict__:
            raise AttributeError("C4FilesToCopy.glob is not set for application %s" % app_name)
        _validate_file_globs((self.glob,), "the C4FilesToCopy.glob field of %s" % app_name)
        if 'destination' not in self.__dict__:
            raise AttributeError("C4FilesToCopy.destination is not set for application %s" % app_name)
        _validate_file_paths((self.destination,), "the C4FilesToCopy.destination field of %s" % app_name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('glob', 'destination'))


class C4RegistryValue(object):
    """Specifies a registry value to set."""

    ### Property defaults

    # The name of the registry value to set.
    # This field is mandatory and thus has no default.
    # name = ""

    # The content of the registry value to set.
    # This field is mandatory and thus has no default.
    # data = ""


    ### Instance validation and dumping

    def validate(self, app_name, key):
        """Checks that all the mandatory fields have been set."""
        if 'name' not in self.__dict__:
            raise AttributeError("C4RegistryValue.name is not set for %s in application %s" % (key, app_name))
        if 'data' not in self.__dict__:
            raise AttributeError("C4RegistryValue.data is not set for %s in application %s" % (key, app_name))
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('name', 'data'))


class C4Registry(object):
    """Specifies a set of registry values to set."""

    ### Property defaults

    # The name of the registry key.
    # This field is mandatory and thus defaults to None.
    key = None

    # The name of the registry value to set if any.
    values = tuple()


    ### Instance validation and dumping

    ROOT_KEYS = frozenset((u'HKEY_CLASSES_ROOT', u'HKCR',
                           u'HKEY_CURRENT_USER', u'HKCU',
                           u'HKEY_LOCAL_MACHINE', u'HKLM',
                           u'HKEY_USERS', u'HKU'))

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if not self.key:
            raise AttributeError("C4Registry.key is not set for application %s" % app_name)
        # pylint: disable=E1135
        if u'\\' not in self.key:
            raise AttributeError("C4Registry.key %s contains no backslashes for application %s" % (self.key, app_name))
        if self.key.split(u'\\', 1)[0] not in self.ROOT_KEYS:
            raise AttributeError("C4Registry.key %s has an invalid root key for application %s" % (self.key, app_name))
        if self.values:
            for value in self.values:
                value.validate(app_name, self.key)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('key', 'values'))


class C4LnkFile(object):
    """Specifies a wnidows shortcut to create."""

    ### Property defaults

    # The path of the shortcut file to create.
    # This field is mandatory and thus defaults to None.
    shortcut = None

    # The path of the file the shortcut should point to.
    # This field is mandatory and thus defaults to None.
    target = None

    # The work directory to store in the shortcut if any.
    workdir = ""


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set."""
        if 'shortcut' not in self.__dict__:
            raise AttributeError("C4LnkFile.shortcut is not set for application %s" % app_name)
        if 'target' not in self.__dict__:
            raise AttributeError("C4LnkFile.target is not set for %s in application %s" % (self.shortcut, app_name))
        _validate_file_paths((self.shortcut, self.target), "the C4LnkFile shortcut or target fields of %s" % app_name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('shortcut', 'target', 'workdir'))


class C4InstallerProfile(object):
    """Contains the C4 Installer Profile information."""

    ### 'Pre-Install' Property defaults

    # Points to the umbrella C4Profile object that ties a C4 entry's
    # specialized profiles together. This field is mandatory and thus defaults
    # to None.
    parent = None

    # A string storing the application id of the profile these settings apply
    # to. Note that it may not match the application id of the parent
    # C4Profile. It is optional and defaults to None.
    appid = None

    # Specifies when this installer profile should be used.
    use_if = None

    # Used to warn users about issues they may encounter during the
    # installation.
    _installation_notes = None

    # A file that should be present on the DVD. If it is missing, then the UDF
    # side has been mounted and it needs remounting.
    udf_remount_check = ""

    # List of names under which the installer may be found on local media.
    local_installer_file_globs = tuple()

    # The list of applications that must be installed before we can install
    # this one.
    pre_dependencies = frozenset()

    # The list of applications that must be installed after installed this one
    # in order for it to actually work.
    post_dependencies = frozenset()

    # Start Menu / Desktop entries which should never appear in our main menu
    mainmenu_never = tuple()

    # The important cxdiag warnings, or those to ignore.
    cxdiag_checks = tuple()

    ### 'Install' Property defaults

    # The file globs to use to find the installer when faced with a directory.
    installer_file_globs = tuple()

    # A full basename to treat the installer as despite what its real name
    # may be
    installer_treatas = ""

    # A list of options to pass to the application installer.
    installer_options = tuple()

    # A list of options to pass to the application installer when doing a
    # silent installation.
    installer_silent_options = tuple()

    # The windows version to use to run the installer.
    installer_winver = ""

    # A Wine DllOverrides string to use to run the installer.
    installer_dlloverrides = ""

    # A list of C4EnvVar objects describing the environment variables to set
    # before running the installer.
    installer_environment = tuple()

    # A size in KB above which the installer is considered to be a
    # self-extracting archive.
    selfextract_threshold = None

    # A list of options to pass to the self-extracting archive to get it to
    # extract in a known location. These options can make use of the special
    # %ExtractDir% environment variable.
    selfextract_options = tuple()

    # A list of options to pass to the self-extracting archive when doing a
    # silent installation.
    selfextract_silent_options = tuple()

    # Specifies the encoding of the file names stored in the zip archive.
    # Blank by default which means no conversion is done.
    zip_encoding = ""

    # A list of C4FilesToCopy objects describing how to copy the files for
    # applications that don't have a proper installer.
    files_to_copy = tuple()

    # If True, force a reboot after the installer completes.
    post_install_reboot = False

    # A list of C4EnvVar objects describing the Unix environment variables to
    # persistently set for the bottle before running the installer.
    pre_install_environment = tuple()

    # A list of C4Registry objects describing the registry keys and values to
    # create before running the installer.
    pre_install_registry = tuple()

    # A list of fake dlls to remove before running the installer.
    pre_rm_fake_dlls = tuple()

    # A list of Netscape plugin dlls to export before running the installer.
    pre_install_nsplugin_dlls = tuple()

    # A list of Netscape plugins to ignore.
    ignore_nsplugin_dlls = tuple()

    # A list of C4LnkFile objects describing the Windows shortcuts to be
    # created.
    lnk_files = tuple()

    ### 'Post-Install' Property defaults

    # Set to True if the application is known to not create any menu.
    skip_menu_creation = False

    # Set to True if the application is known to not create any association.
    skip_assoc_creation = False

    # Set to True if the installer should export browser plugins to the host.
    install_nsplugins = False

    # The list of EAssocs that should be made the default.
    default_eassocs = tuple()

    # The list of EAssocs that should be exported as alternatives.
    alt_eassocs = tuple()

    # A list of dll file paths to register after install
    post_registerdll = tuple()

    # A list of C4Registry objects describing the registry keys and values to
    # create after running the installer.
    post_install_registry = tuple()

    # A web page to open after the application has installed.
    post_install_urls = None

    # Whether to redownload the installer if we have it cached.
    always_redownload = False

    def __init__(self):
        self._installation_notes = {}
        self.pre_dependencies = set()
        self.post_dependencies = set()
        self.post_install_urls = {}

    def copy(self):
        result = C4InstallerProfile()
        result.__dict__.update(self.__dict__)
        _copy_values(result.__dict__)
        return result

    def update(self, oth):
        _update_values(self.__dict__, oth.__dict__, exclude=('parent',), special={'_installation_notes': _update_html_dict})


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set on this profile
        and in the connected profiles.
        """
        if not self.parent:
            raise AttributeError("C4InstallerProfile.parent is not set for application %s" % app_name)
        _validate_file_globs(self.local_installer_file_globs, "the local_installer_file_globs field of %s" % app_name)
        for dependency in self.pre_dependencies:
            if dependency.startswith('.'):
                raise AttributeError("C4InstallerProfile.pre_dependencies '%s' must not start with a dot for application %s" % (dependency, app_name))
        for dependency in self.post_dependencies:
            if dependency.startswith('.'):
                raise AttributeError("C4InstallerProfile.post_dependencies '%s' must not start with a dot for application %s" % (dependency, app_name))
        _validate_file_globs(self.installer_file_globs, "the installer_file_globs field of %s" % app_name)
        _validate_cxdiag_checks(self.cxdiag_checks, "the C4InstallerProfile.cxdiag_checks fields of %s" % app_name)
        for envvar in self.installer_environment:
            envvar.validate(app_name)
        if self.selfextract_threshold is None:
            if self.selfextract_options:
                raise AttributeError("C4InstallerProfile.selfextract_options is set but no threshold has been specified for application %s" % app_name)
            if self.selfextract_silent_options:
                raise AttributeError("C4InstallerProfile.selfextract_silent_options is set but no threshold has been specified for application %s" % app_name)
        for file_copy in self.files_to_copy:
            file_copy.validate(app_name)
        for regentry in self.pre_install_registry:
            regentry.validate(app_name)
        if self.lnk_files:
            for lnk_file in self.lnk_files:
                lnk_file.validate(app_name)
            if self.skip_menu_creation:
                raise AttributeError("C4InstallerProfile.lnk_files must be empty when skip_menu_creation is True for application %s" % app_name)
        if self.skip_assoc_creation and (self.default_eassocs or self.alt_eassocs):
            raise AttributeError("C4InstallerProfile.default_eassocs and alt_eassocs must be empty when skip_assoc_creation is True for application %s" % app_name)
        for eassocs in (self.default_eassocs, self.alt_eassocs):
            for eassocid in eassocs:
                if eassocid[0] != '.' or '\\' in eassocid:
                    raise AttributeError("C4InstallerProfile.default_eassocs and alt_eassocs must must all start with a dot and not contain backslashes (%s) for application %s" % (eassocid, app_name))
        for regentry in self.post_install_registry:
            regentry.validate(app_name)

        return True


    def dump(self, out, indent=""):
        _dump_fields(out, indent, self,
                     ('use_if', 'installation_notes', 'udf_remount_check',
                      'local_installer_file_globs', 'pre_dependencies',
                      'post_dependencies',
                      'cxdiag_checks', 'installer_file_globs',
                      'installer_treatas', 'installer_options',
                      'installer_silent_options', 'installer_winver',
                      'installer_dlloverrides', 'installer_environment',
                      'selfextract_threshold', 'selfextract_options',
                      'selfextract_silent_options', 'zip_encoding',
                      'files_to_copy', 'post_install_reboot',
                      'pre_install_registry', 'pre_rm_fake_dlls',
                      'pre_install_nsplugin_dlls', 'ignore_nsplugin_dlls',
                      'lnk_files', 'skip_menu_creation', 'skip_assoc_creation',
                      'install_nsplugins', 'default_eassocs', 'alt_eassocs',
                      'post_registerdll', 'post_install_urls'))


    ### Accessors for special properties

    def use(self, use_if_props):
        if self.appid is not None and self.appid != use_if_props['appid']:
            return False
        if self.use_if is None:
            return True
        return self.use_if.is_true(use_if_props)


    def _getinstallation_notes(self):
        """Returns the application installation notes in the current language.

        If no installation notes are available for the current language, then
        the default installation notes are returned.
        """
        _lang, installation_notes = cxutils.get_language_value(self._installation_notes)
        return installation_notes

    installation_notes = property(_getinstallation_notes)


    def get_default_eassocs_str(self):
        """Returns a string concatenating all the EAssocs that are to be made
        defaults.

        The string is suitable for cxassoc.
        """
        return ":".join(self.default_eassocs)


    def get_alt_eassocs_str(self):
        """Returns a string concatenating all the EAssocs that are to be made
        alternatives.

        The string is suitable for cxassoc.
        """
        return ":".join(self.alt_eassocs)


    def _getpost_install_url(self):
        """Returns the postinstall web page to open for the current language.

        If no URL is specified for the current language, then the default
        URL is returned.
        """
        _lang, post_install_url = cxutils.get_language_value(self.post_install_urls)
        return post_install_url

    post_install_url = property(_getpost_install_url)


#####
#
# Umbrella C4Profile class
#
#####

class C4CXVersion(object):
    """Identifies a product plus version combination which is compatible with
    the corresponding profile.
    """

    ### Property defaults

    # These class variables provide defaults in case the corresponding
    # instance variable is not set. They should all be immutable objects,

    # A string storing the product name.
    # This field is mandatory and thus defaults to None.
    product = None

    # A string storing the version prefix.
    # This field is mandatory and thus defaults to None.
    cxversion = None


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set on this profile
        and in the connected profiles.

        Note that this only checks internal consistency, not consistency with
        other profiles.
        """
        if not self.product:
            raise AttributeError("C4Profile.product is not set for application %s" % app_name)
        if not self.cxversion:
            raise AttributeError("C4Profile.cxversion is not set for application %s" % app_name)
        # FIXME: We should check that the cxversion field is valid
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('product', 'cxversion'))

    def copy(self):
        result = C4CXVersion()
        result.__dict__.update(self.__dict__)
        _copy_values(result.__dict__)
        return result

    def update(self, oth):
        _update_values(self.__dict__, oth.__dict__)


class C4PTimestamp(object):
    datetime = None

    def __init__(self, timestamp):
        if timestamp is not None:
            self.datetime = iso8601.parse_date(timestamp)

    def __cmp__(self, oth):
        # We want "better" timestamps to be less.

        if not isinstance(oth, C4PTimestamp):
            raise TypeError()

        # If one object has a timestamp, prefer that one.
        if self.datetime is None:
            if oth.datetime is None:
                return 0
            return 1

        if oth.datetime is None:
            return -1

        # Else, we want later timestamps to be lesser, which is the opposite of
        # how datetime objects compare.
        return cmp(oth.datetime, self.datetime)

class C4CXContributor(object):
    """Identifies a contributor to the profile
    """

    ### Property defaults

    # These class variables provide defaults in case the corresponding
    # instance variable is not set. They should all be immutable objects,

    # A string storing the contributor name.
    # This field is mandatory and thus defaults to None.
    name = None

    # A string storing the ID of the contributor in our database
    userid = 0


    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set on this profile
        and in the connected profiles.

        Note that this only checks internal consistency, not consistency with
        other profiles.
        """
        if not self.name:
            raise AttributeError("C4Profile.name is not set for application %s" % app_name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('name', 'userid'))

    def copy(self):
        result = C4CXContributor()
        result.__dict__.update(self.__dict__)
        _copy_values(result.__dict__)
        return result

    def update(self, oth):
        _update_values(self.__dict__, oth.__dict__)

_medal_ratings = {
    'gold': 5,
    'silver': 4,
    'bronze': 3,
    'ungold': 5,
    'unsilver': 4,
    'unbronze': 3,
    'knownnottowork': 1,
    'untested': 0,
    'untrusted': -1,
    }

class C4CXMedal(object):
    """Identifies a contributor to the profile
    """

    ### Property defaults

    # These class variables provide defaults in case the corresponding
    # instance variable is not set. They should all be immutable objects,

    # A string storing the rank, Must be in all medals set.
    # This field is mandatory and thus defaults to None.
    medal = None

    # A string storing the platform this medal is for.
    platform = None

    # A the version this medal was ranked for.
    version = None

    # The number of rankings this medal represents.
    number = 0

    # The timestamp of the last time this medal was ranked.
    timestamp = None

    # Rating from 0 (untested) to 5, if present in the xml
    raw_rating = None

    _rating = None

    ### Instance validation and dumping

    def validate(self, app_name):
        """Checks that all the mandatory fields have been set on this profile
        and in the connected profiles.

        Note that this only checks internal consistency, not consistency with
        other profiles.
        """
        if not self.medal:
            raise AttributeError("C4Profile.medal is not set for application %s" % app_name)
        if self.medal not in C4ApplicationProfile.ALL_MEDALS:
            raise AttributeError("C4Profile.medal invalid medal for application %s" % app_name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('medal', 'platform', 'version', 'number', 'timestamp'))

    def copy(self):
        result = C4CXMedal()
        result.__dict__.update(self.__dict__)
        _copy_values(result.__dict__)
        return result

    def update(self, oth):
        _update_values(self.__dict__, oth.__dict__)

    def _get_rating(self):
        if self._rating is None:
            if self.raw_rating is None or not self.raw_rating.isdigit() or \
                    self.medal == C4ApplicationProfile.UNTRUSTED_MEDAL:
                self._rating = _medal_ratings.get(self.medal)
            else:
                self._rating = int(self.raw_rating)
        return self._rating

    rating = property(_get_rating)

### Helper for computing the profile scores


builtin, download, dropin = range(3)
class C4Profile(object):
    """This is an umbrella class which ties a C4 entry's CD, Application and
    Installer profiles together.

    It also stores the common parts between these, that is the application id
    and its name.
    """

    ### Property defaults

    # These class variables provide defaults in case the corresponding
    # instance variable is not set. They should all be immutable objects,

    # This field does not appear in the C4P file. It reflects if this profile
    # has been parsed from a trusted source or not. It is set by the
    # C4Parser.
    trusted = False

    # This field does not appear in the C4P file. It reflects if this profile
    # originated from the .c4ps that were bundled with CrossOver or if
    # they were added at runtime via an association or drag-n-drop.
    source = builtin

    # At present, this field does not appear in the C4P file.
    # A string representing the origin of the c4profile.
    # It is set by the C4Parser.
    origin = None

    # A string storing the application id, usually a C4 identifier.
    # This field is mandatory and thus defaults to None.
    appid = None

    # The timestamp of the profile's last modification.
    # This field is optional and defaults to None.
    timestamp = None

    # The CrossOver products and versions this profile was tested with.
    cxversions = tuple()

    # A dictionary mapping the language to the application name in that
    # language. The special '' mapping corresponds to the default application
    # name. This field is mandatory and thus defaults to None.
    localized_names = None

    # A list of contributors to the tie file.
    contributor = tuple()

    # Points to the CD profile information if any.
    # In pysetup we may never use this field but it would be used by the CD
    # detection tool.
    cd_profile = None

    # Points to the application profile information if any.
    app_profile = None

    # The list of installer profiles. There may be more than one, because some
    # may be modifying properties of other profiles, and some may only be used
    # in specific conditions.
    installer_profiles = tuple()


    ### Instance validation and dumping

    def validate(self):
        """Checks that all the mandatory fields have been set on this profile
        and in the connected profiles.

        Note that this only checks internal consistency, not consistency with
        other profiles.
        """
        if not self.appid:
            raise AttributeError("C4Profile.appid is not set")
        # pylint: disable=E1101
        elif self.appid.startswith('.'):
            raise AttributeError("C4Profile.appid '%s' must not start with a dot" % self.appid)
        if not _validate_language_field(self.localized_names):
            raise AttributeError("C4Profile.localized_names is not set for %s" % self.appid)
        for cxversion in self.cxversions:
            cxversion.validate(self.name)

        # Pylint does not see that if these are not None, then they will point
        # to instances that do have the validate function()
        # pylint: disable=E1101
        if self.cd_profile:
            self.cd_profile.validate(self.name)
        if self.app_profile:
            self.app_profile.validate(self.name)
        for inst_profile in self.installer_profiles:
            inst_profile.validate(self.name)
        return True

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self,
                     ('appid', 'timestamp', 'cxversions', '_name', 'app_profile',
                      'installer_profiles', 'cd_profile'))


    def copy(self):
        result = C4Profile()
        result.__dict__.update(self.__dict__)
        _copy_values(result.__dict__)
        if result.cd_profile:
            result.cd_profile.parent = result
        if result.app_profile:
            result.app_profile.parent = result
        for inst_profile in result.installer_profiles:
            inst_profile.parent = result

        return result

    def update(self, oth):
        _update_values(self.__dict__, oth.__dict__)
        # The profiles may have been copied, in which case the parent needs to
        # be updated.
        if self.cd_profile:
            self.cd_profile.parent = self
        if self.app_profile:
            self.app_profile.parent = self
        for inst_profile in self.installer_profiles:
            inst_profile.parent = self


    ### Helpers to compute the profile score

    # The product's code and numerical version data
    _product = None
    _product_version = None
    _product_missing = None

    # A cache mapping the (product, cxversion) tuples to the corresponding
    # score
    _scores = {}


    ### Accessors for special properties

    def _getname(self):
        """Returns the application name in the current language.

        If no name is available for the current language, then the
        default name is returned.
        """
        _lang, name = cxutils.get_language_value(self.localized_names)
        return name

    name = property(_getname)

    _score = None


    def _version_score_for_product(self, version, product):
        if version.product == product['productid']:
            product_mismatch = 0
        else:
            product_mismatch = 1

        # This case is a bit of a hack. Since as of CrossOver 11 the cxoffice
        # product has swallowed up the games product, we need to match cxgames
        # CrossTies as well.
        if product['productid'] == 'cxoffice' and version.product == 'cxgames':
            product_mismatch = 0

        if self.source == dropin:
            source = 0
        else:
            source = 1

        if cxutils.cmp_versions(version.cxversion, product['productversion']) > 0:
            old_product = 1
        else:
            old_product = 0

        version_difference = cxutils.subtract_version(version.cxversion, product['productversion'])

        timestamp = C4PTimestamp(self.timestamp)

        return (product_mismatch, source, old_product, version_difference, timestamp)

    def score_for_product(self, product):
        """A score that defines the profile's adequation to the given
        product. The lower the score the better.

        Product is a dictionary containing keys named productid and version.

        The result is actually a tuple of
        (product_mismatch, source, old_product, version_difference, timestamp)

        Where
        * product_mismatch is 0 if the profile matches the current product and 1
          if it does not.
        * old_product is 1 if the product is too old for this profile
        * source is 0 if the profile is a dropin, 1 if it's from a builtin tie
        * version_difference is a tuple of the magnitude of differences between
          components of the version numbers
        * timestamp is a C4PTimestamp object representing the timestamp"""
        return min(self._version_score_for_product(cxversion, product) for cxversion in self.cxversions)

    def _get_score(self):
        """A score that defines the profile's adequation to the current
        product. See score_for_product for the score's meaning."""
        if self._score is None:
            self._score = self.score_for_product(cxproduct.this_product())
        return self._score

    score = property(_get_score)

    def _is_for_current_product(self):
        """Returns True if one of the cxversion properties match the current
        product (though not necessarily this version), and False otherwise.
        """
        if self.score[0] == 1:
            return False
        return True

    is_for_current_product = property(_is_for_current_product)

    def usable_with_product(self, product):
        score = self.score_for_product(product)
        return score[0] == 0 and score[2] == 0


    def _getis_ranked(self):
        if self.app_profile and \
            self.app_profile.medal not in ('knownnottowork', 'untested'):
            return True
        if self.source == dropin:
            return True
        return False

    is_ranked = property(_getis_ranked)


    def _getis_revoked(self):
        revokelist_file = get_revokelist_file()
        if self.appid.encode('utf8') in revokelist_file:
            for timestamp_range in revokelist_file[self.appid.encode('utf8')].values():
                start_timestamp, end_timestamp = timestamp_range.split('..', 1)
                if self.timestamp is None:
                    if start_timestamp == '':
                        return True
                else:
                    timestamp = iso8601.parse_date(self.timestamp)
                    if (start_timestamp == '' or iso8601.parse_date(start_timestamp) <= timestamp) and \
                       (end_timestamp == '' or iso8601.parse_date(end_timestamp) >= timestamp):
                        return True
        return False

    is_revoked = property(_getis_revoked)


    def _getdetails_url(self):
        """Returns the url to a page with more information about this profile,
        or '' if there is no such page."""
        # For now, just treat com.codeweavers.c4.NNN as special.
        # In the future, we may want to add a field in c4p files for this.
        # pylint: disable=E1101
        if self.appid.startswith('com.codeweavers.c4.'):
            c4_id = self.appid[19:]
            if c4_id.isdigit():
                return 'http://www.codeweavers.com/compatibility/browse/name?app_id=%s' % c4_id
        return ''

    details_url = property(_getdetails_url)


    def _is_application(self):
        """Returns True if the application flag is set."""
        # pylint: disable=E1101
        return 'application' in self.app_profile.flags

    is_application = property(_is_application)


    def _is_component(self):
        """Returns True if the component flag is set."""
        # pylint: disable=E1101
        return 'component' in self.app_profile.flags

    is_component = property(_is_component)


    def _is_nsplugin(self):
        """Returns True if the nsplugin flag is set."""
        # pylint: disable=E1101
        return 'nsplugin' in self.app_profile.flags

    is_nsplugin = property(_is_nsplugin)

    def _get_contributors(self):
        contributors = ""
        for contributor in self.contributor:
            if contributors:
                contributors += ", " + contributor.name
            else:
                contributors = contributor.name
        return contributors.encode('utf8')

    contributors = property(_get_contributors)

    def _get_contributors_html(self):
        contributors = ""
        for contributor in self.contributor:
            if contributors:
                if contributor.userid:
                    contributors += ", " + '<a href="http://www.codeweavers.com/profile/?id='+contributor.userid+'">' + cxutils.html_escape(contributor.name.encode('utf8')) + "</a>"
                else:
                    contributors += ", " + cxutils.html_escape(contributor.name.encode('utf8'))
            else:
                if contributor.userid:
                    contributors = '<a href="http://www.codeweavers.com/profile/?id='+contributor.userid+'">' + cxutils.html_escape(contributor.name.encode('utf8')) + "</a>"
                else:
                    contributors = cxutils.html_escape(contributor.name.encode('utf8'))
        return contributors.encode('utf8')

    contributors = property(_get_contributors)
    contributors_html = property(_get_contributors_html)


#####
#
# C4RevokeProfile class
#
#####
class C4RevokeProfile(object):
    """Contains the identifying information of the C4Profile instances to
    revoke.
    """

    ### Property defaults

    # These class variables provide defaults in case the corresponding
    # instance variable is not set. They should all be immutable objects,

    # A string storing the application id, usually a C4 identifier.
    # This field is mandatory and thus defaults to None.
    appid = None

    # The timestamp of the most oldest revision of the profile to revoke.
    # This field is optional and defaults to None.
    first = None

    # The timestamp of the most recent revision of the profile to revoke.
    # This field is optional and defaults to None.
    last = None


    ### Instance validation and dumping

    def validate(self):
        """Checks that all the mandatory fields have been set on this profile.
        """
        if not self.appid:
            raise AttributeError("C4RevokeProfile.appid is not set")

    def dump(self, out, indent=""):
        _dump_fields(out, indent, self, ('appid', 'first', 'last'))


# supported_list is implemented as a singleton -- it
#  will read itself on first access.
#
# supported_list is a simple list of app ids. If supported_list
#  is non-NULL, then /only/ apps in the list should appear
#  with a 'supported' medal in the GUI.
class supported_list(object):
    _supported_list_read = False
    _supported_list = None

    @classmethod
    def ids(cls):
        if not cls._supported_list_read:
            cls.parse_supported_list()
            cls._supported_list_read = True
        return cls._supported_list

    @classmethod
    def parse_supported_list(cls):
        filename = os.path.join(cxutils.CX_ROOT, "share/crossover/data", ".supported")

        try:
            f = open(filename, 'U')
        except IOError:
            # No such file
            return

        try:
            cls._supported_list = []
            for line in f:
                line = line.strip()
                if line != '' and not line.startswith('#'):
                    cls._supported_list.append(line.decode('utf8'))
        finally:
            f.close()


_REVOKELIST_FILE = None

def get_revokelist_file():
    """Returns a cxconfig file used to store all the revokelist entries we've seen."""
    # pylint: disable=W0603
    global _REVOKELIST_FILE
    if _REVOKELIST_FILE is None:
        _REVOKELIST_FILE = cxconfig.File(os.path.join(cxproduct.get_user_dir(), 'tie', 'crossover.revokelist'))
    return _REVOKELIST_FILE