# ----------------------------------------------------------------------
#    Copyright (C) 2018-2020 Christian Boltz <apparmor@cboltz.de>
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of version 2 of the GNU General Public
#    License as published by the Free Software Foundation.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
# ----------------------------------------------------------------------

from apparmor.aare import AARE
from apparmor.common import AppArmorBug, AppArmorException
from apparmor.rule.alias import AliasRule, AliasRuleset
from apparmor.rule.abi import AbiRule, AbiRuleset
from apparmor.rule.include import IncludeRule, IncludeRuleset
from apparmor.rule.variable import VariableRule, VariableRuleset

# setup module translations
from apparmor.translations import init_translation
_ = init_translation()


class ProfileList:
    ''' Stores the preamble section and the list of profile(s) (both name and
        attachment) that live in profile files.

        Also allows "reverse" lookups to find out in which file a profile
        lives.
    '''

    def __init__(self):
        self.profile_names = {}     # profile name -> filename
        self.attachments = {}       # attachment -> filename
        self.attachments_AARE = {}  # AARE(attachment) -> filename
        self.files = {}             # filename -> content - see init_file()

    def __repr__(self):
        return('\n<ProfileList>\n%s\n</ProfileList>\n' % '\n'.join(self.files))

    def init_file(self, filename):
        if self.files.get(filename):
            return  # don't re-initialize / overwrite existing data

        self.files[filename] = {
            'abi': AbiRuleset(),
            'alias': AliasRuleset(),
            'inc_ie': IncludeRuleset(),
            'variable': VariableRuleset(),
            'profiles': [],
        }

    def add_profile(self, filename, profile_name, attachment):
        ''' Add the given profile and attachment to the list '''

        if not filename:
            raise AppArmorBug('Empty filename given to ProfileList')

        if not profile_name and not attachment:
            raise AppArmorBug('Neither profile name or attachment given')

        if profile_name in self.profile_names:
            raise AppArmorException(_('Profile %(profile_name)s exists in %(filename)s and %(filename2)s' % {'profile_name': profile_name, 'filename': filename, 'filename2': self.profile_names[profile_name]}))

        if attachment in self.attachments:
            raise AppArmorException(_('Profile for %(profile_name)s exists in %(filename)s and %(filename2)s' % {'profile_name': attachment, 'filename': filename, 'filename2': self.attachments[attachment]}))

        if profile_name:
            self.profile_names[profile_name] = filename

        if attachment:
            self.attachments[attachment] = filename
            self.attachments_AARE[attachment] = AARE(attachment, True)

        self.init_file(filename)

        if profile_name:
            self.files[filename]['profiles'].append(profile_name)
        else:
            self.files[filename]['profiles'].append(attachment)

    def add_abi(self, filename, abi_rule):
        ''' Store the given abi rule for the given profile filename preamble '''

        if type(abi_rule) is not AbiRule:
            raise AppArmorBug('Wrong type given to ProfileList: %s' % abi_rule)

        self.init_file(filename)

        self.files[filename]['abi'].add(abi_rule)

    def add_alias(self, filename, alias_rule):
        ''' Store the given alias rule for the given profile filename preamble '''

        if type(alias_rule) is not AliasRule:
            raise AppArmorBug('Wrong type given to ProfileList: %s' % alias_rule)

        self.init_file(filename)

        self.files[filename]['alias'].add(alias_rule)

    def add_inc_ie(self, filename, inc_rule):
        ''' Store the given include / include if exists rule for the given profile filename preamble '''
        if type(inc_rule) is not IncludeRule:
            raise AppArmorBug('Wrong type given to ProfileList: %s' % inc_rule)

        self.init_file(filename)

        self.files[filename]['inc_ie'].add(inc_rule)

    def add_variable(self, filename, var_rule):
        ''' Store the given variable rule for the given profile filename preamble '''
        if type(var_rule) is not VariableRule:
            raise AppArmorBug('Wrong type given to ProfileList: %s' % var_rule)

        self.init_file(filename)

        self.files[filename]['variable'].add(var_rule)

    def delete_preamble_duplicates(self, filename):
        ''' Delete duplicates in the preamble of the given profile file '''

        if not self.files.get(filename):
            raise AppArmorBug('%s not listed in ProfileList files' % filename)

        deleted = 0

        for r_type in ['abi', 'alias', 'inc_ie', 'variable']:  # TODO: don't hardcode
            deleted += self.files[filename][r_type].delete_duplicates(None)  # None means not to check includes -- TODO check if this makes sense for all preamble rule types

        return deleted

    def get_raw(self, filename, depth=0):
        ''' Get the preamble for the given profile filename (in original formatting) '''
        if not self.files.get(filename):
            raise AppArmorBug('%s not listed in ProfileList files' % filename)

        data = []
        data += self.files[filename]['abi'].get_raw(depth)
        data += self.files[filename]['alias'].get_raw(depth)
        data += self.files[filename]['inc_ie'].get_raw(depth)
        data += self.files[filename]['variable'].get_raw(depth)
        return data

    def get_clean(self, filename, depth=0):
        ''' Get the preamble for the given profile filename (in clean formatting) '''
        if not self.files.get(filename):
            raise AppArmorBug('%s not listed in ProfileList files' % filename)

        data = []
        data += self.files[filename]['abi'].get_clean_unsorted(depth)
        data += self.files[filename]['alias'].get_clean_unsorted(depth)
        data += self.files[filename]['inc_ie'].get_clean_unsorted(depth)
        data += self.files[filename]['variable'].get_clean_unsorted(depth)
        return data

    def filename_from_profile_name(self, name):
        ''' Return profile filename for the given profile name, or None '''

        return self.profile_names.get(name, None)

    def filename_from_attachment(self, attachment):
        ''' Return profile filename for the given attachment/executable path, or None '''

        if not attachment.startswith( ('/', '@', '{') ):
            raise AppArmorBug('Called filename_from_attachment with non-path attachment: %s' % attachment)

        # plain path
        if self.attachments.get(attachment):
            return self.attachments[attachment]

        # try AARE matches to cover profile names with alternations and wildcards
        for path in self.attachments.keys():
            if self.attachments_AARE[path].match(attachment):
                return self.attachments[path]  # XXX this returns the first match, not necessarily the best one

        return None  # nothing found

    def get_all_merged_variables(self, filename, all_incfiles):
        ''' Get merged variables of a file and its includes

            Note that this function is more forgiving than apparmor_parser.
            It detects variable redefinitions and adding values to non-existing variables.
            However, it doesn't honor the order - so adding to a variable first and defining
            it later won't trigger an error.
        '''

        if not self.files.get(filename):
            raise AppArmorBug('%s not listed in ProfileList files' % filename)

        merged_variables = {}

        mainfile_variables = self.files[filename]['variable'].get_merged_variables()

        # keep track in which file a variable gets set
        set_in = {}
        for var in mainfile_variables['=']:
            merged_variables[var] = mainfile_variables['='][var]
            set_in[var] = filename

        # collect variable additions (+=)
        inc_add = {}
        if mainfile_variables['+=']:
            inc_add[filename] = mainfile_variables['+=']  # variable additions from main file

        for incname in all_incfiles:
            if not self.files.get(incname):
                continue  # tunables/* only end up in self.files if they contain variable or alias definitions

            inc_vars = self.files[incname]['variable'].get_merged_variables()

            for var in inc_vars['=']:
                if merged_variables.get(var):
                    raise AppArmorException('While parsing %(profile)s: Conflicting variable definitions for variable %(var)s found in %(file1)s and %(file2)s.' % {
                            'var': var, 'profile': filename, 'file1': set_in[var], 'file2': incname})
                else:
                    merged_variables[var] = inc_vars['='][var]
                    set_in[var] = incname

            # variable additions can happen in other files than the variable definition. First collect them from all files...
            if inc_vars['+=']:
                inc_add[incname] = inc_vars['+=']

        for incname in inc_add:
            # ... and then check if the variables that get extended have an initial definition. If yes, merge them.
            for var in inc_add[incname]:
                if merged_variables.get(var):
                    merged_variables[var] |= inc_add[incname][var]
                else:
                    raise AppArmorException('While parsing %(profile)s: Variable %(var)s was not previously declared, but is being assigned additional value in file %(file)s.' % {
                            'var': var, 'profile': filename, 'file': incname})

        return merged_variables

    def profiles_in_file(self, filename):
        ''' Return list of profiles in the given file '''
        if not self.files.get(filename):
            raise AppArmorBug('%s not listed in ProfileList files' % filename)

        return self.files[filename]['profiles']
