'''sr_lint.py: lint checks'''
#
# Copyright (C) 2013-2016 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3 of the License.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import print_function
from clickreviews.sr_common import (
    SnapReview,
)
from clickreviews.common import (
    find_external_symlinks,
)
import glob
import os
import re


class SnapReviewLint(SnapReview):
    '''This class represents snap lint reviews'''

    def __init__(self, fn, overrides=None):
        '''Set up the class.'''
        SnapReview.__init__(self, fn, "lint-snap-v2", overrides=overrides)
        if not self.is_snap2:
            return

        self.valid_compiled_architectures = ['armhf',
                                             'i386',
                                             'amd64',
                                             'arm64',
                                             'ppc64el',
                                             's390x',
                                             ]
        self.valid_architectures = ['all'] + self.valid_compiled_architectures
        self.vcs_files = ['.bzr*',
                          # '.excludes',  # autogenerated by SDK
                          '.git*',
                          '.idea',
                          '.svn*',
                          '.hg',
                          '.project',
                          'CVS*',
                          'RCS*'
                          ]

        self._list_all_compiled_binaries()

        # Valid values for 'type' in packaging yaml
        # - app
        # - kernel
        # - gadget
        # - os
        self.valid_snap_types = ['app',
                                 'kernel',
                                 'gadget',
                                 'os',
                                 ]
        self.redflagged_snap_types = ['kernel',
                                      'gadget',
                                      'os',
                                      ]

    def check_architectures(self):
        '''Check architectures in snap.yaml is valid'''
        if not self.is_snap2:
            return

        t = 'info'
        n = self._get_check_name('architecture_valid')
        s = 'OK'

        key = 'architectures'
        if key not in self.snap_yaml:
            s = 'OK (%s not specified)' % key
            self._add_result(t, n, s)
            return

        if not isinstance(self.snap_yaml[key], list):
            t = 'error'
            s = "invalid %s entry: %s (not a list)" % (key,
                                                       self.snap_yaml[key])
        else:
            bad_archs = []
            for arch in self.snap_yaml[key]:
                if arch not in self.valid_architectures:
                    bad_archs.append(arch)
                if len(bad_archs) > 0:
                    t = 'error'
                    s = "invalid multi architecture: %s" % ",".join(bad_archs)
        self._add_result(t, n, s)

    def check_description(self):
        '''Check description'''
        if not self.is_snap2:
            return

        key = 'description'

        t = 'info'
        n = self._get_check_name('%s_present' % key)
        s = 'OK'
        if key not in self.snap_yaml:
            s = 'OK (optional %s field not specified)' % key
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name(key)
        s = 'OK'
        if not isinstance(self.snap_yaml[key], str):
            t = 'error'
            s = "invalid %s entry: %s (not a str)" % (key, self.snap_yaml[key])
            self._add_result(t, n, s)
            return
        elif len(self.snap_yaml[key]) < 1:
            t = 'error'
            s = "invalid %s entry (empty)" % (key)
        elif len(self.snap_yaml[key]) < len(self.snap_yaml['name']):
            t = 'info'
            s = "%s is too short: '%s'" % (key, self.snap_yaml[key])
        self._add_result(t, n, s)

    # TODO: verify this is a field
    def check_license_agreement(self):
        '''Check license-agreement'''
        if not self.is_snap2:
            return

        key = 'license-agreement'

        t = 'info'
        n = self._get_check_name('%s_present' % key)
        s = 'OK'
        if key not in self.snap_yaml:
            s = 'OK (optional %s field not specified)' % key
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name(key)
        s = 'OK'
        if not isinstance(self.snap_yaml[key], str):
            t = 'error'
            s = "invalid %s entry: %s (not a str)" % (key, self.snap_yaml[key])
            self._add_result(t, n, s)
            return
        elif len(self.snap_yaml[key]) < 1:
            t = 'error'
            s = "invalid %s entry (empty)" % (key)
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

    def check_license_version(self):
        '''license-version'''
        if not self.is_snap2:
            return

        key = 'license-version'
        t = 'info'
        n = self._get_check_name('%s_present' % key)
        s = 'OK'
        if key not in self.snap_yaml:
            s = 'OK (optional %s field not specified)' % key
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name(key)
        s = 'OK'
        if not isinstance(self.snap_yaml[key], str):
            t = 'error'
            s = "invalid %s entry: %s (not a str)" % (key, self.snap_yaml[key])
            self._add_result(t, n, s)
            return
        elif len(self.snap_yaml[key]) < 1:
            t = 'error'
            s = "invalid %s entry (empty)" % (key)
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

    def check_name(self):
        '''Check package name'''
        if not self.is_snap2:
            return

        t = 'info'
        n = self._get_check_name('name_valid')
        s = 'OK'
        if 'name' not in self.snap_yaml:
            t = 'error'
            s = "could not find 'name' in yaml"
        elif not isinstance(self.snap_yaml['name'], str):
            t = 'error'
            s = "malformed 'name': %s (not a str)" % (self.snap_yaml['name'])
        elif not self._verify_pkgname(self.snap_yaml['name']):
            t = 'error'
            s = "malformed 'name': '%s'" % self.snap_yaml['name']
        self._add_result(t, n, s)

    def check_summary(self):
        '''Check summary'''
        if not self.is_snap2:
            return

        key = 'summary'

        t = 'info'
        n = self._get_check_name('%s_present' % key)
        s = 'OK'
        if key not in self.snap_yaml:
            s = 'OK (optional %s field not specified)' % key
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name(key)
        s = 'OK'
        if not isinstance(self.snap_yaml[key], str):
            t = 'error'
            s = "invalid %s entry: %s (not a str)" % (key, self.snap_yaml[key])
            self._add_result(t, n, s)
            return
        elif len(self.snap_yaml[key]) < 1:
            t = 'error'
            s = "invalid %s entry (empty)" % (key)
        elif len(self.snap_yaml[key]) < len(self.snap_yaml['name']):
            t = 'info'
            s = "%s is too short: '%s'" % (key, self.snap_yaml[key])
        self._add_result(t, n, s)

    def check_type(self):
        '''Check type'''
        if not self.is_snap2 or 'type' not in self.snap_yaml:
            return

        t = 'info'
        n = self._get_check_name('snap_type_valid')
        s = 'OK'
        if self.snap_yaml['type'] not in self.valid_snap_types:
            t = 'error'
            s = "unknown 'type': '%s'" % self.snap_yaml['type']
        self._add_result(t, n, s)

    def check_type_redflagged(self):
        '''Check if type is redflagged'''
        if not self.is_snap2 or 'type' not in self.snap_yaml:
            return

        t = 'info'
        n = self._get_check_name('snap_type_redflag')
        s = "OK"
        manual_review = False
        if self.snap_yaml['type'] in self.redflagged_snap_types:
            t = 'error'
            s = "(NEEDS REVIEW) type '%s' not allowed" % self.snap_yaml['type']
            manual_review = True
        self._add_result(t, n, s, manual_review=manual_review)

    def check_version(self):
        '''Check package version'''
        if not self.is_snap2:
            return

        t = 'info'
        n = self._get_check_name('version_valid')
        s = 'OK'
        if 'version' not in self.snap_yaml:
            t = 'error'
            s = "could not find 'version' in yaml"
        elif not self._verify_pkgversion(self.snap_yaml['version']):
            t = 'error'
            s = "malformed 'version': '%s'" % self.snap_yaml['version']
        self._add_result(t, n, s)

    def check_config(self):
        '''Check config'''
        if not self.is_snap2:
            return

        fn = os.path.join(self._get_unpack_dir(), 'meta/hooks/config')
        if fn not in self.pkg_files:
            return

        t = 'info'
        n = self._get_check_name('config_hook_executable')
        s = 'OK'
        if not self._check_innerpath_executable(fn):
            t = 'error'
            s = 'meta/hooks/config is not executable'
        self._add_result(t, n, s)

    def check_icon(self):
        '''Check icon'''
        # see docs/meta.md and docs/gadget.md
        if not self.is_snap2 or 'icon' not in self.snap_yaml:
            return

        # Snappy icons may be specified in the gadget snap.yaml, but not in
        # app snap.yaml. With apps, the icon may be in meta/gui/icon.png and
        # this file is optional. Therefore, for apps, there is nothing to do.
        t = 'info'
        n = self._get_check_name('icon_present')
        s = 'OK'
        if 'type' in self.snap_yaml and self.snap_yaml['type'] != "gadget":
            t = 'warn'
            s = 'icon only used with gadget snaps'
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name('icon_empty')
        s = 'OK'
        if len(self.snap_yaml['icon']) == 0:
            t = 'error'
            s = "icon entry is empty"
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name('icon_absolute_path')
        s = 'OK'
        if self.snap_yaml['icon'].startswith('/'):
            t = 'error'
            s = "icon entry '%s' should not specify absolute path" % \
                self.snap_yaml['icon']
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name('icon_exists')
        s = 'OK'
        fn = self._path_join(self._get_unpack_dir(), self.snap_yaml['icon'])
        if fn not in self.pkg_files:
            t = 'error'
            s = "icon entry '%s' does not exist" % self.snap_yaml['icon']
        self._add_result(t, n, s)

    def check_unknown_entries(self):
        '''Check for any unknown fields'''
        if not self.is_snap2:
            return

        t = 'info'
        n = self._get_check_name('unknown_field')
        s = 'OK'
        unknown = []
        for f in self.snap_yaml:
            if f not in self.snappy_required + self.snappy_optional:
                unknown.append(f)
        if len(unknown) > 0:
            t = 'warn'
            s = "unknown entries in snap.yaml: '%s'" % \
                (",".join(sorted(unknown)))
        self._add_result(t, n, s)

    def check_apps(self):
        '''Check apps'''
        if not self.is_snap2:
            return

        key = 'apps'

        t = 'info'
        n = self._get_check_name('%s_present' % key)
        s = 'OK'
        if key not in self.snap_yaml:
            s = 'OK (optional %s field not specified)' % key
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name(key)
        s = 'OK'
        if not isinstance(self.snap_yaml[key], dict):
            t = 'error'
            s = "invalid %s entry: %s (not a dict)" % (key,
                                                       self.snap_yaml[key])
            self._add_result(t, n, s)
            return
        elif len(self.snap_yaml[key].keys()) < 1:
            t = 'error'
            s = "invalid %s entry (empty)" % (key)
            self._add_result(t, n, s)
            return
        self._add_result(t, n, s)

        for app in self.snap_yaml[key]:
            t = 'info'
            n = self._get_check_name('%s_entry' % key, app=app)
            s = 'OK'

            if not isinstance(self.snap_yaml[key][app], dict):
                t = 'error'
                s = "invalid entry: %s (not a dict)" % (
                    self.snap_yaml[key][app])
                self._add_result(t, n, s)
                continue
            elif len(self.snap_yaml[key][app].keys()) < 1:
                t = 'error'
                s = "invalid entry for '%s' (empty)" % (app)
                self._add_result(t, n, s)
                continue
            elif not self._verify_appname(app):
                t = 'error'
                s = "malformed app name: '%s'" % app
                self._add_result(t, n, s)
                continue
            self._add_result(t, n, s)

            for field in self.apps_required:
                t = 'info'
                n = self._get_check_name('%s_required' % key, app=app)
                s = 'OK'
                if field not in self.snap_yaml[key][app]:
                    t = 'error'
                    s = "required field '%s' not specified" % field
                self._add_result(t, n, s)

            t = 'info'
            n = self._get_check_name('%s_unknown' % key, app=app)
            s = 'OK'
            unknown = []
            for field in self.snap_yaml[key][app]:
                if field not in self.apps_required + self.apps_optional:
                    unknown.append(field)
            if len(unknown) > 0:
                t = 'warn'
                s = "unknown fields for app '%s': '%s'" % (
                    app, ",".join(sorted(unknown)))
            self._add_result(t, n, s)

    def _verify_value_is_file(self, app, key):
            t = 'info'
            n = self._get_check_name('%s' % key, app=app)
            s = 'OK'
            if not isinstance(self.snap_yaml['apps'][app][key], str):
                t = 'error'
                s = "%s '%s' (not a str)" % (key,
                                             self.snap_yaml['apps'][app][key])
                self._add_result(t, n, s)
            elif len(self.snap_yaml['apps'][app][key]) < 1:
                t = 'error'
                s = "invalid %s (empty)" % (key)
                self._add_result(t, n, s)
            else:
                fn = self._path_join(self._get_unpack_dir(),
                                     os.path.normpath(
                                         self.snap_yaml['apps'][app][key]))
                if fn not in self.pkg_files:
                    print("does not exist")
                    print(self.pkg_files)
                    t = 'error'
                    s = "%s does not exist" % (
                        self.snap_yaml['apps'][app][key])
            self._add_result(t, n, s)

    def check_apps_command(self):
        '''Check apps - command'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'command'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            self._verify_value_is_file(app, key)

    def check_apps_stop_command(self):
        '''Check apps - stop-command'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'stop-command'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            self._verify_value_is_file(app, key)

    def check_apps_post_stop_command(self):
        '''Check apps - post-stop-command'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'post-stop-command'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            self._verify_value_is_file(app, key)

    def check_apps_stop_timeout(self):
        '''Check apps - stop-timeout'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'stop-timeout'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            t = 'info'
            n = self._get_check_name('%s' % key, app=app)
            s = "OK"
            if not isinstance(self.snap_yaml['apps'][app][key], int) and \
                    not isinstance(self.snap_yaml['apps'][app][key], str):
                t = 'error'
                s = "'%s' is not a string or integer" % key
            elif not re.search(r'[0-9]+[ms]?$',
                               str(self.snap_yaml['apps'][app][key])):
                t = 'error'
                s = "'%s' is not of form NN[ms] (%s)" % \
                    (self.snap_yaml['apps'][app][key], key)
            self._add_result(t, n, s)

            if t == 'error':
                continue

            t = 'info'
            n = self._get_check_name('%s_range' % key, app=app)
            s = "OK"
            st = int(str(self.snap_yaml['apps'][app][key]).rstrip(r'[ms]'))
            if st < 0 or st > 60:
                t = 'error'
                s = "stop-timeout '%d' out of range (0-60)" % \
                    self.snap_yaml['apps'][app][key]
            self._add_result(t, n, s)

    def _verify_valid_values(self, app, key, valid):
        '''Verify valid values for key in app'''
        t = 'info'
        n = self._get_check_name('%s' % key, app=app)
        s = 'OK'
        if not isinstance(self.snap_yaml['apps'][app][key], str):
            t = 'error'
            s = "%s '%s' (not a str)" % (key,
                                         self.snap_yaml['apps'][app][key])
            self._add_result(t, n, s)
        elif len(self.snap_yaml['apps'][app][key]) < 1:
            t = 'error'
            s = "invalid %s (empty)" % (key)
            self._add_result(t, n, s)
        elif self.snap_yaml['apps'][app][key] not in valid:
            t = 'error'
            s = "invalid %s: '%s'" % (key, self.snap_yaml['apps'][app][key])
        self._add_result(t, n, s)

    def check_apps_daemon(self):
        '''Check apps - daemon'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        valid = ["simple",
                 "forking",
                 "oneshot",
                 "dbus",
                 ]

        for app in self.snap_yaml['apps']:
            key = 'daemon'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            self._verify_valid_values(app, key, valid)

    def check_apps_nondaemon(self):
        '''Check apps - non-daemon'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        # Certain options require 'daemon' so list the keys that are shared
        # by services and binaries
        ok_keys = ['command', 'plugs']

        for app in self.snap_yaml['apps']:
            needs_daemon = []
            for key in self.snap_yaml['apps'][app]:
                if key not in self.apps_optional or \
                        key == 'daemon' or \
                        key in ok_keys or \
                        'daemon' in self.snap_yaml['apps'][app]:
                    continue
                needs_daemon.append(key)

            t = 'info'
            n = self._get_check_name('daemon_required', app=app)
            s = "OK"
            if len(needs_daemon) > 0:
                t = 'error'
                s = "'%s' must be used with 'daemon'" % ",".join(needs_daemon)
            self._add_result(t, n, s)

    def check_apps_restart_condition(self):
        '''Check apps - restart-condition'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        valid = ["always",
                 "never",
                 "on-abnormal",
                 "on-abort",
                 "on-failure",
                 "on-success",
                 ]

        for app in self.snap_yaml['apps']:
            key = 'restart-condition'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            self._verify_valid_values(app, key, valid)

    def check_apps_ports(self):
        '''Check apps - ports'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        valid_keys = ['internal', 'external']
        valid_subkeys = ['port', 'negotiable']
        for app in self.snap_yaml['apps']:
            if 'ports' not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            t = 'info'
            n = self._get_check_name('ports', app=app)
            s = 'OK'
            l = None
            if not isinstance(self.snap_yaml['apps'][app]['ports'], dict):
                t = 'error'
                s = "ports '%s' (not a dict)" % (
                    self.snap_yaml['apps'][app]['ports'])
            elif len(self.snap_yaml['apps'][app]['ports'].keys()) < 1:
                t = 'error'
                s = "'ports' must contain 'internal' and/or 'external'"
            self._add_result(t, n, s, l)
            if t == 'error':
                continue

            # unknown
            unknown = []
            for key in self.snap_yaml['apps'][app]['ports']:
                if key not in valid_keys:
                    unknown.append(key)
            if len(unknown) > 0:
                t = 'error'
                n = self._get_check_name('ports_unknown_key', extra=key,
                                         app=app)
                s = "Unknown '%s' for ports" % (",".join(unknown))
                self._add_result(t, n, s)

            port_pat = re.compile(r'^[0-9]+/[a-z0-9\-]+$')
            for key in valid_keys:
                if key not in self.snap_yaml['apps'][app]['ports']:
                    continue

                if len(self.snap_yaml['apps'][app]['ports'][key].keys()) < 1:
                    t = 'error'
                    n = self._get_check_name('ports', extra=key, app=app)
                    s = 'Could not find any %s ports' % key
                    self._add_result(t, n, s)
                    continue

                for tagname in self.snap_yaml['apps'][app]['ports'][key]:
                    entry = self.snap_yaml['apps'][app]['ports'][key][tagname]
                    if len(entry.keys()) < 1 or ('negotiable' not in entry and
                                                 'port' not in entry):
                        t = 'error'
                        n = self._get_check_name('ports', extra=key, app=app)
                        s = "Could not find 'port' or 'negotiable' in '%s'" % \
                            tagname
                        self._add_result(t, n, s)
                        continue

                    # unknown
                    unknown = []
                    for subkey in entry:
                        if subkey not in valid_subkeys:
                            unknown.append(subkey)
                    if len(unknown) > 0:
                        t = 'error'
                        n = self._get_check_name('ports_unknown_subkey',
                                                 extra=key, app=app)
                        s = "Unknown '%s' for %s" % (",".join(unknown),
                                                     tagname)
                        self._add_result(t, n, s)

                    # port
                    subkey = 'port'
                    t = 'info'
                    n = self._get_check_name('ports_%s_format' % tagname,
                                             extra=subkey)
                    s = 'OK'
                    if subkey not in entry:
                        s = 'OK (skipped, not found)'
                    elif not isinstance(entry[subkey], str):
                        t = 'error'
                        s = "invalid entry: %s (not a str)" % (entry[subkey])
                    else:
                        tmp = entry[subkey].split('/')
                        if not port_pat.search(entry[subkey]) or \
                           int(tmp[0]) < 1 or int(tmp[0]) > 65535:
                            t = 'error'
                            s = "'%s' should be of form " % entry[subkey] + \
                                "'port/protocol' where port is an integer " + \
                                "(1-65535) and protocol is found in " + \
                                "/etc/protocols"
                    self._add_result(t, n, s)

                    # negotiable
                    subkey = 'negotiable'
                    t = 'info'
                    n = self._get_check_name('ports_%s_format' % tagname,
                                             extra=subkey)
                    s = 'OK'
                    if subkey not in entry:
                        s = 'OK (skipped, not found)'
                    elif not isinstance(entry[subkey], bool):
                        t = 'error'
                        s = "'%s: %s' should be either 'yes' or 'no'" % \
                            (subkey, entry[subkey])
                    self._add_result(t, n, s)

    def check_apps_socket(self):
        '''Check apps - socket'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'socket'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            t = 'info'
            n = self._get_check_name(key, app=app)
            s = 'OK'
            if not isinstance(self.snap_yaml['apps'][app][key], bool):
                t = 'error'
                s = "'%s: %s' should be either 'yes' or 'no'" % (
                    key, self.snap_yaml['apps'][app][key])
            elif 'listen-stream' not in self.snap_yaml['apps'][app]:
                t = 'error'
                s = "'socket' specified without 'listen-stream'"
            self._add_result(t, n, s)

    def check_apps_listen_stream(self):
        '''Check apps - listen-stream'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'listen-stream'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            t = 'info'
            n = self._get_check_name(key, app=app)
            s = 'OK'
            if not isinstance(self.snap_yaml['apps'][app][key], str):
                t = 'error'
                s = "invalid entry: %s (not a str)" % (
                    self.snap_yaml['apps'][app][key])
            elif len(self.snap_yaml['apps'][app][key]) == 0:
                t = 'error'
                s = "'%s' is empty" % key
            self._add_result(t, n, s)
            if t == 'error':
                continue

            t = 'info'
            n = self._get_check_name('%s_matches_name' % key, app=app)
            s = 'OK'
            sock = self.snap_yaml['apps'][app][key]
            pkgname = self.snap_yaml['name']
            if sock.startswith('@'):
                if sock != '@%s' % pkgname and \
                        not sock.startswith('@%s_' % pkgname):
                    t = 'error'
                    s = ("abstract socket '%s' is neither '%s' nor starts "
                         "with '%s'" % (sock, '@%s' % pkgname,
                                        '@%s_' % pkgname))
            elif sock.startswith('/'):
                found = False
                for path in ["/tmp/",
                             "/var/lib/snaps/%s/" % pkgname,
                             "/var/lib/snaps/%s." % pkgname,
                             "/run/shm/snaps/%s/" % pkgname,
                             "/run/shm/snaps/%s." % pkgname]:
                    if sock.startswith(path):
                        found = True
                        break
                if not found:
                    t = 'error'
                    s = ("named socket '%s' should be in a writable "
                         "app-specific area or /tmp" % sock)
            else:
                t = 'error'
                s = ("'%s' does not specify an abstract socket (starts "
                     "with '@') or absolute filename" % (sock))
            self._add_result(t, n, s)

    def _verify_valid_socket(self, app, key):
        '''Verify valid values for socket key'''
        t = 'info'
        n = self._get_check_name(key, app=app)
        s = 'OK'
        if not isinstance(self.snap_yaml['apps'][app][key], str):
            t = 'error'
            s = "invalid entry: %s (not a str)" % (
                self.snap_yaml['apps'][app][key])
        elif len(self.snap_yaml['apps'][app][key]) == 0:
            t = 'error'
            s = "'%s' is empty" % key
        elif 'listen-stream' not in self.snap_yaml['apps'][app]:
            t = 'error'
            s = "'%s' specified without 'listen-stream'" % key
        self._add_result(t, n, s)
        if t == 'error':
            return

        t = 'error'
        n = self._get_check_name('%s_reserved' % key, app=app)
        s = "'%s' should not be used until snappy supports per-app users" \
            % key
        self._add_result(t, n, s)

        t = 'info'
        n = self._get_check_name("%s_matches_name" % key, app=app)
        s = 'OK'
        if self.snap_yaml['apps'][app][key] != self.snap_yaml['name']:
            t = 'error'
            s = "'%s' != '%s'" % (self.snap_yaml['apps'][app][key],
                                  self.snap_yaml['name'])
        self._add_result(t, n, s)

    def check_apps_socket_user(self):
        '''Check apps - socket-user'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'socket-user'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            self._verify_valid_socket(app, key)

    def check_apps_socket_group(self):
        '''Check apps - socket-group'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'socket-group'
            if key not in self.snap_yaml['apps'][app]:
                # We check for required elsewhere
                continue

            self._verify_valid_socket(app, key)

    def _verify_interfaces(self, iface_type):
        for iface in self.snap_yaml[iface_type]:
            # If the 'interface' name is the same as the 'iface' name, then
            # 'interface' is optional since the interface name and the iface
            # name are the same
            interface = iface
            if 'interface' in self.snap_yaml[iface_type][iface]:
                interface = self.snap_yaml[iface_type][iface]['interface']

                key = 'interface'
                t = 'info'
                n = self._get_check_name(iface_type, app=key, extra=iface)
                s = 'OK'
                if not isinstance(self.snap_yaml[iface_type][iface][key], str):
                    t = 'error'
                    s = "invalid %s: %s (not a str)" % \
                        (key, self.snap_yaml[iface_type][iface][key])
                elif len(self.snap_yaml[iface_type][iface][key]) == 0:
                    t = 'error'
                    s = "'%s' is empty" % key
                self._add_result(t, n, s)
                if t == 'error':
                    continue

            t = 'info'
            n = self._get_check_name(iface_type, app=interface, extra=iface)
            s = 'OK'
            if interface not in self.interfaces:
                t = 'error'
                s = "unknown interface '%s'" % interface
            self._add_result(t, n, s)
            if t == 'error':
                continue

            min = 1
            if 'interface' in self.snap_yaml[iface_type][iface] and \
                    interface in self.interfaces and \
                    len(self.interfaces[interface]) > 0:
                min = 2
            t = 'info'
            n = self._get_check_name(iface_type, extra='attributes')
            s = 'OK'
            if len(self.snap_yaml[iface_type][iface]) < min:
                t = 'error'
                s = "'%s' has no attributes" % iface
            self._add_result(t, n, s)
            if t == 'error':
                continue

            for attrib in self.snap_yaml[iface_type][iface]:
                if attrib == 'interface':
                    continue
                t = 'info'
                n = self._get_check_name('%s_attributes' % iface_type,
                                         app=iface, extra=attrib)
                s = "OK"
                if attrib not in self.interfaces[interface]:
                    t = 'error'
                    s = "unknown attribute '%s' for interface '%s'" % (
                        attrib, interface)
                elif not isinstance(self.snap_yaml[iface_type][iface][attrib],
                                    type(self.interfaces[interface][attrib])):
                    t = 'error'
                    s = "'%s' is not '%s'" % \
                        (attrib,
                         type(self.interfaces[interface][attrib]).__name__)
                self._add_result(t, n, s)

    def check_plugs(self):
        '''Check plugs'''
        iface_type = 'plugs'
        if not self.is_snap2 or iface_type not in self.snap_yaml:
            return

        self._verify_interfaces(iface_type)

    def _verify_app_interfaces(self, app, key):
        t = 'info'
        n = self._get_check_name("app_%s" % key, app=app)
        s = "OK"
        if not isinstance(self.snap_yaml['apps'][app][key], list):
            t = 'error'
            s = "invalid '%s' entry: '%s' (not a list)" % (
                key, self.snap_yaml['apps'][app][key])
        elif len(self.snap_yaml['apps'][app][key]) < 1:
            t = 'error'
            s = "invalid %s entry (empty)" % (key)
        self._add_result(t, n, s)
        if t == 'error':
            return

        # The interface referenced in the app's 'key' field (plugs/slots) can
        # either be a known interface (when the interface name reference and
        # the interface is the same) or can reference a name in the snap's
        # toplevel 'key' (plugs/slots) mapping
        for ref in self.snap_yaml['apps'][app][key]:
            t = 'info'
            n = self._get_check_name('app_%s_plug_reference' % key,
                                     app=app,
                                     extra=ref)
            s = "OK"
            if not isinstance(ref, str):
                t = 'error'
                s = "invalid %s interface name reference: '%s' (not a str)" \
                    % (key, ref)
            elif ref not in self.interfaces and \
                    (key not in self.snap_yaml or
                     ref not in self.snap_yaml[key]):
                t = 'error'
                s = "unknown %s interface name reference '%s'" % (key, ref)
            self._add_result(t, n, s)

    def check_apps_plugs(self):
        '''Check apps plugs'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'plugs'
            if key not in self.snap_yaml['apps'][app]:
                continue

            self._verify_app_interfaces(app, key)

    def check_slots(self):
        '''Check slots'''
        iface_type = 'slots'
        if not self.is_snap2 or iface_type not in self.snap_yaml:
            return

        self._verify_interfaces(iface_type)

    def check_apps_slots(self):
        '''Check apps slots'''
        if not self.is_snap2 or 'apps' not in self.snap_yaml:
            return

        for app in self.snap_yaml['apps']:
            key = 'slots'
            if key not in self.snap_yaml['apps'][app]:
                continue

            self._verify_app_interfaces(app, key)

    def check_external_symlinks(self):
        '''Check snap for external symlinks'''
        if not self.is_snap2:
            return

        # Note: unclear if kernel and gadget snaps can legitimately have
        # external symlinks, but err on side of caution
        if 'type' in self.snap_yaml and self.snap_yaml['type'] == 'os':
            return

        t = 'info'
        n = self._get_check_name('external_symlinks')
        s = 'OK'
        links = find_external_symlinks(self._get_unpack_dir(), self.pkg_files)
        if len(links) > 0:
            t = 'error'
            s = 'package contains external symlinks: %s' % ', '.join(links)
        self._add_result(t, n, s)

    def check_architecture_all(self):
        '''Check if actually architecture all'''
        if not self.is_snap2:
            return

        if 'architectures' in self.snap_yaml and \
                'all' not in self.snap_yaml['architectures']:
            return

        t = 'info'
        n = self._get_check_name('valid_contents_for_architecture')
        s = 'OK'

        # look for compiled code
        x_binaries = []
        for i in self.pkg_bin_files:
            x_binaries.append(os.path.relpath(i, self._get_unpack_dir()))
        if len(x_binaries) > 0:
            t = 'error'
            s = "found binaries for architecture 'all': %s" % \
                ", ".join(x_binaries)
        self._add_result(t, n, s)

    def check_architecture_specified_needed(self):
        '''Check if the specified architecture is actually needed'''
        if not self.is_snap2 or 'architectures' not in self.snap_yaml:
            return

        if 'all' in self.snap_yaml['architectures']:
            return

        for arch in self.snap_yaml['architectures']:
            t = 'info'
            n = self._get_check_name('architecture_specified_needed',
                                     extra=arch)
            s = 'OK'
            if len(self.pkg_bin_files) == 0:
                t = 'warn'
                s = "Could not find compiled binaries for architecture '%s'" \
                    % arch
            self._add_result(t, n, s)

    def check_vcs(self):
        '''Check for VCS files in the package'''
        if not self.is_snap2:
            return

        t = 'info'
        n = self._get_check_name('vcs_files')
        s = 'OK'
        found = []
        for d in self.vcs_files:
            entries = glob.glob("%s/%s" % (self._get_unpack_dir(), d))
            if len(entries) > 0:
                for i in entries:
                    found.append(os.path.relpath(i, self.unpack_dir))
        if len(found) > 0:
            t = 'warn'
            s = 'found VCS files in package: %s' % ", ".join(found)
        self._add_result(t, n, s)
