'''sr_lint.py: lint checks'''
#
# Copyright (C) 2013-2017 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,
    STORE_PKGNAME_SNAPV2_MAXLEN,
)
from clickreviews.overrides import (
    redflagged_snap_types_overrides,
    desktop_file_exception
)
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',
                                             'powerpc',
                                             '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.iffy_files = [
            r'^\..+\.swp$',  # vim
        ]

        self._list_all_compiled_binaries()

        self.redflagged_snap_types = ['kernel',
                                      'gadget',
                                      'os',
                                      ]

        self.interface_plug_requires_desktop_file = ['unity7',
                                                     'x11',
                                                     'unity8'
                                                     ]

    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 not isinstance(arch, str):
                    bad_archs.append(str(arch))
                elif 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_assumes(self):
        '''Check assumes in snap.yaml is valid'''
        if not self.is_snap2:
            return

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

        key = 'assumes'
        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_assumes = []
            for a in self.snap_yaml[key]:
                if not isinstance(a, str):
                    bad_assumes.append(str(a))
            if len(bad_assumes) > 0:
                t = 'error'
                s = "invalid assumes: %s" % ",".join(bad_assumes)
        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)

    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 len(self.snap_yaml['name']) > STORE_PKGNAME_SNAPV2_MAXLEN:
            t = 'error'
            s = "malformed 'name': '%s' " % self.snap_yaml['name'] + \
                "(length '%d' " % len(self.snap_yaml['name']) + \
                "exceeds store limit '%d')" % STORE_PKGNAME_SNAPV2_MAXLEN
        elif not self._verify_pkgname(self.snap_yaml['name']):
            t = 'error'
            s = "malformed 'name': '%s' " % self.snap_yaml['name'] + \
                "(may be only lower case, digits and hyphens)"
        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:
            pkgname = self.snap_yaml['name']
            snaptype = self.snap_yaml['type']
            if snaptype in redflagged_snap_types_overrides and \
                    pkgname in redflagged_snap_types_overrides[snaptype]:
                s = "OK (override '%s' for 'type: %s')" % (pkgname,
                                                           snaptype)
            else:
                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 _verify_apps_and_hooks(self, hook=False):
        key = 'apps'
        key_type = 'app'
        required = self.apps_required
        optional = self.apps_optional
        if hook:
            key = 'hooks'
            key_type = 'hook'
            required = self.hooks_required
            optional = self.hooks_optional

        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 val in self.snap_yaml[key]:
            t = 'info'
            n = self._get_check_name('%s_entry' % key, app=val)
            s = 'OK'

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

            for field in required:
                t = 'info'
                n = self._get_check_name('%s_required' % key, app=val)
                s = 'OK'
                if field not in self.snap_yaml[key][val]:
                    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=val)
            s = 'OK'
            unknown = []
            for field in self.snap_yaml[key][val]:
                if field not in required + optional:
                    unknown.append(field)
            if len(unknown) > 0:
                t = 'warn'
                s = "unknown fields for %s '%s': '%s'" % (
                    key_type, val, ",".join(sorted(unknown)))
            self._add_result(t, n, s)

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

    def check_hooks(self):
        '''Check hooks'''
        if not self.is_snap2:
            return
        self._verify_apps_and_hooks(hook=True)

    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:
                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

            suffix = 'ns|us|ms|s|m'
            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]+(%s)?$' % suffix,
                               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'(%s)' %
                                                                  suffix))
            if st < 0:
                t = 'error'
                s = "stop-timeout '%s' should be a positive" % \
                    str(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",
                 "notify",
                 ]

        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', 'completer', 'environment', 'plugs', 'slots',
                   'aliases']

        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'
            link = 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, link)
            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

            spec = self.snap_yaml[iface_type][iface]
            if isinstance(spec, str):
                # Abbreviated syntax (no attributes)
                # <plugs|slots>:
                #   <alias>: <interface>
                interface = spec
            elif 'interface' in spec:
                # Full specification.
                # <plugs|slots>:
                #   <alias>:
                #     interface: <interface>
                interface = spec['interface']

            # Validate indirect (specified via alias) interfaces:
            if interface != iface:
                key = 'interface'
                t = 'info'
                n = self._get_check_name(iface_type, app=key, extra=interface)
                s = 'OK'
                if not isinstance(interface, str):
                    t = 'error'
                    s = "invalid %s: %s (not a str)" % (key, interface)
                elif len(interface) == 0:
                    t = 'error'
                    s = "'%s' is empty" % key
                self._add_result(t, n, s)
                if t == 'error':
                    continue

            # Check interfaces whitelist.
            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

            # Abbreviated interfaces don't have attributes, done checking.
            if isinstance(spec, str):
                continue

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

            # Check required interface attributes
            for req_iface in self.interfaces_required:
                if req_iface != interface or \
                   iface_type not in self.interfaces_required[interface]:
                    continue  # pragma: nocover

                has_required = False
                for req_attrib in \
                        self.interfaces_required[interface][iface_type]:
                    combo = req_attrib.split('/')
                    num_combos = 0     # total number of required attribs
                    found = 0          # required attribs found
                    found_invalid = 0  # must-not-have attribs found

                    for r in combo:
                        if not r.startswith('!'):
                            num_combos += 1

                        if r in spec:
                            found += 1
                        elif r.startswith('!') and r[1:] in spec:
                            found_invalid += 1

                    # if no invalid tokens and we found all the required
                    # attribs then we have a valid combination of required
                    # attribs
                    if found_invalid == 0 and found == num_combos:
                        has_required = True
                        break

                t = 'info'
                n = self._get_check_name('%s_required_attributes' % iface_type,
                                         app=iface)
                if not has_required:
                    # format the combinations for human review
                    combos = []
                    for req_attrib in \
                            self.interfaces_required[interface][iface_type]:
                        combo = []
                        for r in req_attrib.split('/'):
                            if not r.startswith('!'):
                                combo.append(r)
                        combos.append("/".join(combo))
                    t = 'error'
                    s = "missing required %s " % iface_type + \
                        "attributes for interface '%s' " % interface + \
                        "(%s)" % ", ".join(combos)
                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_and_hook_interfaces(self, val, key, hook=False):
        topkey = 'apps'
        topkey_type = 'app'
        if hook:
            topkey = 'hooks'
            topkey_type = 'hook'

        t = 'info'
        n = self._get_check_name("%s_%s" % (topkey_type, key), app=val)
        s = "OK"
        if not isinstance(self.snap_yaml[topkey][val][key], list):
            t = 'error'
            s = "invalid '%s' entry: '%s' (not a list)" % (
                key, self.snap_yaml[topkey][val][key])
        elif len(self.snap_yaml[topkey][val][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 entry'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[topkey][val][key]:
            t = 'info'
            n = self._get_check_name('%s_%s_plug_reference' %
                                     (topkey_type, key),
                                     app=val,
                                     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)
            if t == 'error':
                continue

    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_and_hook_interfaces(app, key)

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

        for hook in self.snap_yaml['hooks']:
            key = 'plugs'
            if self.snap_yaml['hooks'][hook] is None \
                    or key not in self.snap_yaml['hooks'][hook]:
                continue

            self._verify_app_and_hook_interfaces(hook, key, hook=True)

    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_and_hook_interfaces(app, key)

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

        # Note: unclear if gadget snaps can legitimately have external
        # symlinks, but err on side of caution. kernel snaps for reference
        # kernels ship a dangling lib/modules/.../build (like on desktop) but
        # also may have legitimate symlinks in lib/firmware, so just allow
        # them.
        if 'type' in self.snap_yaml and (self.snap_yaml['type'] == 'os' or
                                         self.snap_yaml['type'] == 'kernel'):
            return

        t = 'info'
        n = self._get_check_name('external_symlinks')
        s = 'OK'
        links = find_external_symlinks(self._get_unpack_dir(), self.pkg_files,
                                       self.snap_yaml['name'])
        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:
            # .pyc files are arch-independent
            if i.endswith(".pyc"):
                continue
            x_binaries.append(os.path.relpath(i, self._get_unpack_dir()))
        if len(x_binaries) > 0:
            # gadget snap is specified with 'all' but has binaries. Don't
            # complain about that
            t = 'error'
            ok_text = ''
            if 'type' in self.snap_yaml and self.snap_yaml['type'] == 'gadget':
                t = 'info'
                ok_text = " (ok for 'type: gadget')"
            s = "found binaries for architecture 'all': %s%s" % \
                (", ".join(x_binaries), ok_text)
        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:
                # This should be a warning but it causes friction for uploads
                t = 'info'
                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)

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

        t = 'info'
        n = self._get_check_name('iffy_files')
        s = 'OK'
        found = []

        for f in self.pkg_files:
            fn = os.path.basename(f)
            for d in self.iffy_files:
                if re.search(r'%s' % d, fn):
                    found.append(os.path.relpath(f, self.unpack_dir))

        if len(found) > 0:
            t = 'warn'
            s = 'found potentially sensitive files in package: %s' % \
                ", ".join(found)
        self._add_result(t, n, s)

    def check_epoch(self):
        '''Check epoch'''
        if not self.is_snap2 or 'epoch' not in self.snap_yaml:
            return

        t = 'info'
        n = self._get_check_name('epoch_valid')
        s = 'OK'
        if not isinstance(self.snap_yaml['epoch'], int):
            t = 'error'
            s = "malformed 'epoch': %s (not an integer)" % (
                self.snap_yaml['epoch'])
        elif int(self.snap_yaml['epoch']) < 0:
            t = 'error'
            s = "malformed 'epoch': '%s' should be positive integer" % (
                self.snap_yaml['epoch'])
        self._add_result(t, n, s)

    def check_confinement(self):
        '''Check confinement'''
        if not self.is_snap2 or 'confinement' not in self.snap_yaml:
            return

        allowed = ['strict', 'devmode', 'classic']
        use_with = ['app', 'gadget', 'kernel']

        t = 'info'
        n = self._get_check_name('confinement_valid')
        s = 'OK'
        manual_review = False
        if not isinstance(self.snap_yaml['confinement'], str):
            t = 'error'
            s = "malformed 'confinement': %s (not a string)" % (
                self.snap_yaml['confinement'])
        elif self.snap_yaml['confinement'] not in allowed:
            t = 'error'
            s = "malformed 'confinement': '%s' should be one of '%s'" % (
                self.snap_yaml['confinement'], ", ".join(allowed))
        elif self.snap_yaml['type'] not in use_with:
            t = 'info'
            s = "'confinement' should not be used with 'type: %s'" % \
                self.snap_yaml['type']
        self._add_result(t, n, s)

        if self.snap_yaml['confinement'] == "classic":
            t = 'info'
            n = self._get_check_name('confinement_classic')
            s = 'OK'
            manual_review = False
            if self.overrides is not None and \
                    'snap_allow_classic' in self.overrides and \
                    self.overrides['snap_allow_classic']:
                s = "OK (confinement '%s' allowed)" % \
                    self.snap_yaml['confinement']
            else:
                t = 'error'
                s = "(NEEDS REVIEW) confinement '%s' not allowed" % \
                    self.snap_yaml['confinement']
                manual_review = True

            self._add_result(t, n, s, manual_review=manual_review)

            t = 'info'
            n = self._get_check_name('confinement_classic_with_interfaces')
            s = 'OK'
            link = None
            found = False
            if 'plugs' in self.snap_yaml or 'slots' in self.snap_yaml:
                found = True
            elif 'apps' in self.snap_yaml:
                for app in self.snap_yaml['apps']:
                    if 'plugs' in self.snap_yaml['apps'][app] or \
                            'slots' in self.snap_yaml['apps'][app]:
                        found = True
                        break
            if found:
                t = 'error'
                s = "confinement '%s' not allowed with plugs/slots" % \
                    self.snap_yaml['confinement']
                link = "https://launchpad.net/bugs/1655369"

            self._add_result(t, n, s, link=link)

    def check_grade(self):
        '''Check confinement'''
        if not self.is_snap2 or 'grade' not in self.snap_yaml:
            return

        allowed = ['stable', 'devel']
# We may reintroduce this, but for now, grade may be used with all snaps
#         use_with = ['app', 'gadget', 'kernel', 'os']

        t = 'info'
        n = self._get_check_name('grade_valid')
        s = 'OK'
        if not isinstance(self.snap_yaml['grade'], str):
            t = 'error'
            s = "malformed 'grade': %s (not a string)" % (
                self.snap_yaml['grade'])
        elif self.snap_yaml['grade'] not in allowed:
            t = 'error'
            s = "malformed 'grade': '%s' should be one of '%s'" % (
                self.snap_yaml['grade'], ", ".join(allowed))
#         elif self.snap_yaml['type'] not in use_with:
#             # "devel" grade os-snap should be avoided.
#             t = 'warn'
#             s = "'grade' should not be used with 'type: %s'" % \
#                 self.snap_yaml['type']
        self._add_result(t, n, s)

    def _verify_env(self, env, app=None):
        t = 'info'
        n = self._get_check_name('environment_valid', app=app)
        s = 'OK'

        if not isinstance(env, dict):
            t = 'error'
            s = "invalid environment: %s (not a dict)" % env
        self._add_result(t, n, s)

        # http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
        invalid = ['=', '\0']
        portable_pat = re.compile(r'^[A-Z_][A-Z0-9_]*$')
        lenient_pat = re.compile(r'^[a-zA-Z0-9_]+$')
        for key in env:
            t = 'info'
            n = self._get_check_name('environment_key_valid', app=app,
                                     extra=key)
            s = 'OK'
            link = None
            invalid_chars = []
            for c in invalid:
                if c in key:
                    invalid_chars.append(c)

            if len(invalid_chars) > 0:
                t = 'error'
                s = "found invalid characters '%s'" % ", ".join(invalid_chars)
            elif not portable_pat.search(key) and lenient_pat.search(key):
                t = 'info'
                s = "'%s' is not shell portable" % key
                link = ("http://pubs.opengroup.org/onlinepubs/000095399/"
                        "basedefs/xbd_chap08.html")
            elif not lenient_pat.search(key):
                t = 'warn'
                s = "unusual characters in '%s' " % key + \
                    "(should be '^[a-zA-Z0-9_]+$')"
            self._add_result(t, n, s, link=link)

            # The only limit on the contents of an arg appear to be the length
            # but that is going to be language and system dependent, so don't
            # worry about it here (this would simply be a bug in the software)
            t = 'info'
            n = self._get_check_name('environment_value_valid', app=app,
                                     extra=key)
            s = 'OK'
            if not isinstance(env[key], str) and \
                    not isinstance(env[key], int) and \
                    not isinstance(env[key], float):
                t = 'error'
                s = "invalid environment value for '%s': %s" % (key, env[key])
            self._add_result(t, n, s)

    def check_environment(self):
        '''Check environment'''
        if not self.is_snap2 or 'environment' not in self.snap_yaml:
            return

        self._verify_env(self.snap_yaml['environment'])

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

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

            self._verify_env(self.snap_yaml['apps'][app]['environment'],
                             app=app)

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

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

            aliases = self.snap_yaml['apps'][app]['aliases']

            t = 'info'
            n = self._get_check_name('aliases_valid', app=app)
            s = 'OK'
            if not isinstance(aliases, list):
                t = 'error'
                s = "invalid aliases: %s (not a list)" % aliases
            elif len(aliases) == 0:
                t = 'error'
                s = 'invalid aliases (empty)'
            self._add_result(t, n, s)

            # from validate.go in snapd
            pat = re.compile(r'^[a-zA-Z0-9][-_.a-zA-Z0-9]*$')
            for alias in aliases:
                t = 'info'
                n = self._get_check_name('alias_valid', app=app,
                                         extra=alias)
                if not pat.search(alias):
                    t = 'error'
                    s = "malformed alias '%s' " % alias + \
                        "(should be '^[a-zA-Z0-9][-_.a-zA-Z0-9]*$')"
                elif alias in seen:
                    t = 'error'
                    s = "alias '%s' used more than once" % alias
                self._add_result(t, n, s)
                seen.append(alias)

    def _uses_interface(self, iface_type, iface):
        '''Get interface name by type and interface/interface reference.
           Returns:
           - The dereferenced interface name
           - None if could not be found
        '''
        if iface_type in self.snap_yaml:
            for ref in self.snap_yaml[iface_type]:
                interface = ''
                spec = self.snap_yaml[iface_type][ref]
                if isinstance(spec, str):
                    # Abbreviated syntax (no attributes)
                    # <plugs|slots>:
                    #   <alias>: <interface>
                    interface = spec
                elif 'interface' in spec:
                    # Full specification.
                    # <plugs|slots>:
                    #   <alias>:
                    #     interface: <interface>
                    interface = spec['interface']
                elif isinstance(spec, dict):
                    # Abbreviated syntax (no attributes)
                    # <plugs|slots>:
                    #   <interface>: null
                    if len(spec) == 0:
                        interface = ref
                if interface == iface:
                    return True

        if 'apps' in self.snap_yaml:
            for app in self.snap_yaml['apps']:
                if iface_type in self.snap_yaml['apps'][app] and \
                        iface in self.snap_yaml['apps'][app][iface_type]:
                    return True

        return False

    def _verify_desktop_file(self, fn):
        '''Verify the desktop file'''
        if 'apps' not in self.snap_yaml:
            return

        appnames = []
        for app in self.snap_yaml['apps']:
            if app == self.snap_yaml['name']:
                appnames.append(app)
            else:
                appnames.append("%s.%s" % (self.snap_yaml['name'], app))

        fh = self._extract_file(fn)

        # For now, just check Exec= since snapd strips out anything it
        # doesn't understand. TODO: implement full checks
        found_exec = False
        t = 'info'
        n = self._get_check_name('desktop_file',
                                 extra=os.path.basename(fn))
        s = 'OK'
        for line in fh.readlines():
            line = line.rstrip()
            if line.startswith('Exec='):
                found_exec = True
                break
        if not found_exec:
            t = 'error'
            s = "Could not find 'Exec=' in desktop file"
        self._add_result(t, n, s)

    def check_meta_gui_desktop(self):
        '''Check meta/gui/*.desktop'''
        if not self.is_snap2:
            return

        has_desktop_files = False
        for f in self.pkg_files:
            fn = os.path.relpath(f, self._get_unpack_dir())
            if fn.startswith("meta/gui/") and fn.endswith(".desktop"):
                self._verify_desktop_file(f)
                has_desktop_files = True
                break

        desktop_interfaces_specified = []
        for iface in self.interface_plug_requires_desktop_file:
            if self._uses_interface("plugs", iface):
                desktop_interfaces_specified.append(iface)

        if len(desktop_interfaces_specified) < 1 and not has_desktop_files:
            return

        t = 'info'
        n = self._get_check_name('meta_gui_desktop')
        s = 'OK'
        if len(desktop_interfaces_specified) > 0 and not has_desktop_files:
            if self.snap_yaml['name'] in desktop_file_exception:
                t = 'info'
                s = "OK (overidden)"
            else:
                t = 'warn'
                s = ("desktop interfaces "
                     "(%s) " % ",".join(desktop_interfaces_specified) +
                     "specified without a corresponding meta/gui/*.desktop "
                     "file. If using snapcraft, please see "
                     "https://snapcraft.io/docs/build-snaps/metadata"
                     "#fixed-assets. "
                     "Otherwise, please provide a desktop file in "
                     "meta/gui/*.desktop (it should reference one of the "
                     "'apps' from your snapcraft/snap.yaml).")

        self._add_result(t, n, s)

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

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

            self._verify_value_is_file(app, key)
