Example #1
0
    def _check_forbidden_controlchar(self, pkg):
        """Trigger check forbidden-controlchar-found

        Check if package contains a forbidden_words or character in
        tags: Provides, Conflicts, Obsoletes,
        Supplements, Suggests, Enhances, Recommends and Requires

        Returns:
            Output info to STDOUT
        """

        for tagname, items in (('Provides', pkg.provides), ('Conflicts',
                                                            pkg.conflicts),
                               ('Obsoletes', pkg.obsoletes), ('Supplements',
                                                              pkg.supplements),
                               ('Suggests', pkg.suggests),
                               ('Enhances', pkg.enhances), ('Recommends',
                                                            pkg.recommends)):
            for item in items:
                dep = Pkg.has_forbidden_controlchars(item)
                if dep:
                    self.output.add_info('E', pkg,
                                         'forbidden-controlchar-found',
                                         '{}: {}'.format(tagname, dep))
                value = Pkg.formatRequire(*item)
                self._unexpanded_macros(pkg, '{} {}'.format(tagname, value),
                                        value)

            # Check if a package contains forbidden-controlchar in Requires: tag.
            for pkg_token in (pkg.requires):
                dep = Pkg.has_forbidden_controlchars(pkg_token)
                if dep:
                    self.output.add_info('E', pkg,
                                         'forbidden-controlchar-found',
                                         'Requires: {}'.format(dep))
Example #2
0
    def check(self, pkg):

        packager = pkg[rpm.RPMTAG_PACKAGER]
        if packager:
            self._unexpanded_macros(pkg, 'Packager', packager)
            if self.config.configuration['Packager'] and \
               not self.packager_regex.search(packager):
                self.output.add_info('W', pkg, 'invalid-packager', packager)
        else:
            self.output.add_info('E', pkg, 'no-packager-tag')

        version = pkg[rpm.RPMTAG_VERSION]
        if version:
            self._unexpanded_macros(pkg, 'Version', version)
            res = invalid_version_regex.search(version)
            if res:
                self.output.add_info('E', pkg, 'invalid-version', version)
        else:
            self.output.add_info('E', pkg, 'no-version-tag')

        release = pkg[rpm.RPMTAG_RELEASE]
        if release:
            self._unexpanded_macros(pkg, 'Release', release)
            if self.release_ext and not self.extension_regex.search(release):
                self.output.add_info('W', pkg,
                                     'not-standard-release-extension', release)
        else:
            self.output.add_info('E', pkg, 'no-release-tag')

        epoch = pkg[rpm.RPMTAG_EPOCH]
        if epoch is None:
            if self.use_epoch:
                self.output.add_info('E', pkg, 'no-epoch-tag')
        else:
            if epoch > 99:
                self.output.add_info('W', pkg, 'unreasonable-epoch', epoch)
            epoch = str(epoch)

        if self.use_epoch:
            for tag in ('obsoletes', 'conflicts', 'provides', 'recommends',
                        'suggests', 'enhances', 'supplements'):
                for x in (x for x in getattr(pkg, tag)()
                          if x[1] and x[2][0] is None):
                    self.output.add_info('W', pkg, 'no-epoch-in-%s' % tag,
                                         Pkg.formatRequire(*x))

        name = pkg.name
        deps = pkg.requires + pkg.prereq
        devel_depend = False
        is_devel = FilesCheck.devel_regex.search(name)
        is_source = pkg.is_source
        for d in deps:
            value = Pkg.formatRequire(*d)
            if self.use_epoch and d[1] and d[2][0] is None and \
                    not d[0].startswith('rpmlib('):
                self.output.add_info('W', pkg, 'no-epoch-in-dependency', value)
            for r in self.invalid_requires:
                if r.search(d[0]):
                    self.output.add_info('E', pkg, 'invalid-dependency', d[0])

            if d[0].startswith('/usr/local/'):
                self.output.add_info('E', pkg, 'invalid-dependency', d[0])

            if is_source:
                if lib_devel_number_regex.search(d[0]):
                    self.output.add_info('E', pkg, 'invalid-build-requires',
                                         d[0])
            elif not is_devel:
                if not devel_depend and FilesCheck.devel_regex.search(d[0]):
                    self.output.add_info('E', pkg, 'devel-dependency', d[0])
                    devel_depend = True
                if not d[1]:
                    res = lib_package_regex.search(d[0])
                    if res and not res.group(1):
                        self.output.add_info('E', pkg,
                                             'explicit-lib-dependency', d[0])

            if d[1] == rpm.RPMSENSE_EQUAL and d[2][2] is not None:
                self.output.add_info('W', pkg, 'requires-on-release', value)
            self._unexpanded_macros(pkg, 'dependency %s' % (value, ), value)

        self._unexpanded_macros(pkg, 'Name', name)
        if not name:
            self.output.add_info('E', pkg, 'no-name-tag')
        else:
            if is_devel and not is_source:
                base = is_devel.group(1)
                dep = None
                has_so = False
                has_pc = False
                for fname in pkg.files:
                    if fname.endswith('.so'):
                        has_so = True
                    if pkg_config_regex.match(fname) and fname.endswith('.pc'):
                        has_pc = True
                if has_so:
                    base_or_libs = base + '*' + '/' + base + '-libs/lib' + base + '*'
                    # try to match *%_isa as well (e.g. '(x86-64)', '(x86-32)')
                    base_or_libs_re = re.compile(
                        r'^(lib)?%s(-libs)?[\d_-]*(\(\w+-\d+\))?$' %
                        re.escape(base))
                    for d in deps:
                        if base_or_libs_re.match(d[0]):
                            dep = d
                            break
                    if not dep:
                        self.output.add_info('W', pkg, 'no-dependency-on',
                                             base_or_libs)
                    elif version:
                        exp = (epoch, version, None)
                        sexp = Pkg.versionToString(exp)
                        if not dep[1]:
                            self.output.add_info('W', pkg,
                                                 'no-version-dependency-on',
                                                 base_or_libs, sexp)
                        elif dep[2][:2] != exp[:2]:
                            self.output.add_info(
                                'W', pkg, 'incoherent-version-dependency-on',
                                base_or_libs,
                                Pkg.versionToString(
                                    (dep[2][0], dep[2][1], None)), sexp)
                    res = devel_number_regex.search(name)
                    if not res:
                        self.output.add_info('W', pkg, 'no-major-in-name',
                                             name)
                    else:
                        if res.group(3):
                            prov = res.group(1) + res.group(2) + '-devel'
                        else:
                            prov = res.group(1) + '-devel'

                        if prov not in (x[0] for x in pkg.provides):
                            self.output.add_info('W', pkg, 'no-provides', prov)

                if has_pc:
                    found_pkg_config_dep = False
                    for p in (x[0] for x in pkg.provides):
                        if p.startswith('pkgconfig('):
                            found_pkg_config_dep = True
                            break
                    if not found_pkg_config_dep:
                        self.output.add_info('E', pkg,
                                             'no-pkg-config-provides')

        # List of words to ignore in spell check
        ignored_words = set()
        for pf in pkg.files:
            ignored_words.update(pf.split('/'))
        ignored_words.update((x[0] for x in pkg.provides))
        ignored_words.update((x[0] for x in pkg.requires))
        ignored_words.update((x[0] for x in pkg.conflicts))
        ignored_words.update((x[0] for x in pkg.obsoletes))

        langs = pkg[rpm.RPMTAG_HEADERI18NTABLE]

        summary = byte_to_string(pkg[rpm.RPMTAG_SUMMARY])
        if summary:
            if not langs:
                self._unexpanded_macros(pkg, 'Summary', summary)
            else:
                for lang in langs:
                    self.check_summary(pkg, lang, ignored_words)
        else:
            self.output.add_info('E', pkg, 'no-summary-tag')

        description = byte_to_string(pkg[rpm.RPMTAG_DESCRIPTION])
        if description:
            if not langs:
                self._unexpanded_macros(pkg, '%description', description)
            else:
                for lang in langs:
                    self.check_description(pkg, lang, ignored_words)

            if len(description) < len(pkg[rpm.RPMTAG_SUMMARY]):
                self.output.add_info('W', pkg,
                                     'description-shorter-than-summary')
        else:
            self.output.add_info('E', pkg, 'no-description-tag')

        group = pkg[rpm.RPMTAG_GROUP]
        self._unexpanded_macros(pkg, 'Group', group)
        if not group:
            self.output.add_info('E', pkg, 'no-group-tag')
        elif pkg.name.endswith(
                '-devel') and not group.startswith('Development/'):
            self.output.add_info('W', pkg,
                                 'devel-package-with-non-devel-group', group)
        elif self.valid_groups and group not in self.valid_groups:
            self.output.add_info('W', pkg, 'non-standard-group', group)

        buildhost = pkg[rpm.RPMTAG_BUILDHOST]
        self._unexpanded_macros(pkg, 'BuildHost', buildhost)
        if not buildhost:
            self.output.add_info('E', pkg, 'no-buildhost-tag')
        elif self.config.configuration['ValidBuildHost'] and \
                not self.valid_buildhost_regex.search(buildhost):
            self.output.add_info('W', pkg, 'invalid-buildhost', buildhost)

        changelog = pkg[rpm.RPMTAG_CHANGELOGNAME]
        if not changelog:
            self.output.add_info('E', pkg, 'no-changelogname-tag')
        else:
            clt = pkg[rpm.RPMTAG_CHANGELOGTEXT]
            if self.use_version_in_changelog:
                ret = changelog_version_regex.search(
                    byte_to_string(changelog[0]))
                if not ret and clt:
                    # we also allow the version specified as the first
                    # thing on the first line of the text
                    ret = changelog_text_version_regex.search(
                        byte_to_string(clt[0]))
                if not ret:
                    self.output.add_info('W', pkg,
                                         'no-version-in-last-changelog')
                elif version and release:
                    srpm = pkg[rpm.RPMTAG_SOURCERPM] or ''
                    # only check when source name correspond to name
                    if srpm[0:-8] == '%s-%s-%s' % (name, version, release):
                        expected = [version + '-' + release]
                        if epoch is not None:  # regardless of use_epoch
                            expected[0] = str(epoch) + ':' + expected[0]
                        # Allow EVR in changelog without release extension,
                        # the extension is often a macro or otherwise dynamic.
                        if self.release_ext:
                            expected.append(
                                self.extension_regex.sub('', expected[0]))
                        if ret.group(1) not in expected:
                            if len(expected) == 1:
                                expected = expected[0]
                            self.output.add_info(
                                'W', pkg, 'incoherent-version-in-changelog',
                                ret.group(1), expected)

            if clt:
                changelog = changelog + clt
            for s in changelog:
                if not Pkg.is_utf8_bytestr(s):
                    self.output.add_info('E', pkg, 'tag-not-utf8',
                                         '%changelog')
                    break
                e = Pkg.has_forbidden_controlchars(s)
                if e:
                    self.output.add_info('E', pkg,
                                         'forbidden-controlchar-found',
                                         '%%changelog : %s' % e)
                    break

            clt = pkg[rpm.RPMTAG_CHANGELOGTIME][0]
            if clt:
                clt -= clt % (24 * 3600)  # roll back to 00:00:00, see #246
                if clt < oldest_changelog_timestamp:
                    self.output.add_info(
                        'W', pkg, 'changelog-time-overflow',
                        time.strftime('%Y-%m-%d', time.gmtime(clt)))
                elif clt > time.time():
                    self.output.add_info(
                        'E', pkg, 'changelog-time-in-future',
                        time.strftime('%Y-%m-%d', time.gmtime(clt)))

        def split_license(text):
            return (x.strip()
                    for x in (l for l in license_regex.split(text) if l))

        def split_license_exception(text):
            x, y = license_exception_regex.split(text)[1:3] or (text, '')
            return x.strip(), y.strip()

        rpm_license = pkg[rpm.RPMTAG_LICENSE]
        if not rpm_license:
            self.output.add_info('E', pkg, 'no-license')
        else:
            valid_license = True
            if rpm_license not in self.valid_licenses:
                license_string = rpm_license

                l1, lexception = split_license_exception(rpm_license)
                # SPDX allows "<license> WITH <license-exception>"
                if lexception:
                    license_string = l1
                    if lexception not in self.valid_license_exceptions:
                        self.output.add_info('W', pkg,
                                             'invalid-license-exception',
                                             lexception)
                        valid_license = False

                for l1 in split_license(license_string):
                    if l1 in self.valid_licenses:
                        continue
                    for l2 in split_license(l1):
                        if l2 not in self.valid_licenses:
                            self.output.add_info('W', pkg, 'invalid-license',
                                                 l2)
                            valid_license = False
            if not valid_license:
                self._unexpanded_macros(pkg, 'License', rpm_license)

        for tag in ('URL', 'DistURL', 'BugURL'):
            if hasattr(rpm, 'RPMTAG_%s' % tag.upper()):
                url = byte_to_string(pkg[getattr(rpm,
                                                 'RPMTAG_%s' % tag.upper())])
                self._unexpanded_macros(pkg, tag, url, is_url=True)
                if url:
                    (scheme, netloc) = urlparse(url)[0:2]
                    if not scheme or not netloc or '.' not in netloc or \
                            scheme not in ('http', 'https', 'ftp') or \
                            (self.config.configuration['InvalidURL'] and
                             self.invalid_url_regex.search(url)):
                        self.output.add_info('W', pkg, 'invalid-url', tag, url)
                elif tag == 'URL':
                    self.output.add_info('W', pkg, 'no-url-tag')

        obs_names = [x[0] for x in pkg.obsoletes]
        prov_names = [x[0] for x in pkg.provides]

        for o in (x for x in obs_names if x not in prov_names):
            self.output.add_info('W', pkg, 'obsolete-not-provided', o)
        for o in pkg.obsoletes:
            value = Pkg.formatRequire(*o)
            self._unexpanded_macros(pkg, 'Obsoletes %s' % (value, ), value)

        # TODO: should take versions, <, <=, =, >=, > into account here
        #       https://bugzilla.redhat.com/460872
        useless_provides = set()
        for p in prov_names:
            if (prov_names.count(p) != 1 and not p.startswith('debuginfo(')
                    and p not in useless_provides):
                useless_provides.add(p)
        for p in sorted(useless_provides):
            self.output.add_info('E', pkg, 'useless-provides', p)

        for tagname, items in (('Provides', pkg.provides), ('Conflicts',
                                                            pkg.conflicts),
                               ('Obsoletes', pkg.obsoletes), ('Supplements',
                                                              pkg.supplements),
                               ('Suggests', pkg.suggests),
                               ('Enhances', pkg.enhances), ('Recommends',
                                                            pkg.recommends)):
            for p in items:
                e = Pkg.has_forbidden_controlchars(p)
                if e:
                    self.output.add_info('E', pkg,
                                         'forbidden-controlchar-found',
                                         '%s: %s' % (tagname, e))
                value = Pkg.formatRequire(*p)
                self._unexpanded_macros(pkg, '%s %s' % (tagname, value), value)

        for p in (pkg.requires):
            e = Pkg.has_forbidden_controlchars(p)
            if e:
                self.output.add_info('E', pkg, 'forbidden-controlchar-found',
                                     'Requires: %s' % e)

        obss = pkg.obsoletes
        if obss:
            provs = pkg.provides
            for prov in provs:
                for obs in obss:
                    if Pkg.rangeCompare(obs, prov):
                        self.output.add_info(
                            'W', pkg, 'self-obsoletion',
                            '%s obsoletes %s' % (Pkg.formatRequire(*obs),
                                                 Pkg.formatRequire(*prov)))

        expfmt = rpm.expandMacro('%{_build_name_fmt}')
        if pkg.is_source:
            # _build_name_fmt often (always?) ends up not outputting src/nosrc
            # as arch for source packages, do it ourselves
            expfmt = re.sub(r'(?i)%\{?ARCH\b\}?', pkg.arch, expfmt)
        expected = pkg.header.sprintf(expfmt).split('/')[-1]
        basename = Path(pkg.filename).parent
        if basename != expected:
            self.output.add_info('W', pkg, 'non-coherent-filename', basename,
                                 expected)

        for tag in ('Distribution', 'DistTag', 'ExcludeArch', 'ExcludeOS',
                    'Vendor'):
            if hasattr(rpm, 'RPMTAG_%s' % tag.upper()):
                res = byte_to_string(pkg[getattr(rpm,
                                                 'RPMTAG_%s' % tag.upper())])
                self._unexpanded_macros(pkg, tag, res)
Example #3
0
    def check_spec(self, pkg):
        self._spec_file = pkg.name
        spec_only = isinstance(pkg, Pkg.FakePkg)
        spec_lines = Pkg.readlines(self._spec_file)
        patches = {}
        applied_patches = []
        applied_patches_ifarch = []
        patches_auto_applied = False
        source_dir = False
        buildroot = False
        configure_linenum = None
        configure_cmdline = ''
        mklibname = False
        is_lib_pkg = False
        if_depth = 0
        ifarch_depth = -1
        current_section = 'package'
        buildroot_clean = {'clean': False, 'install': False}
        depscript_override = False
        depgen_disabled = False
        patch_fuzz_override = False
        indent_spaces = 0
        indent_tabs = 0
        section = {}
        # None == main package
        current_package = None
        package_noarch = {}

        if self._spec_file:
            if not Pkg.is_utf8(self._spec_file):
                self.output.add_info('E', pkg, 'non-utf8-spec-file',
                                     self._spec_name or self._spec_file)

        # gather info from spec lines

        pkg.current_linenum = 0

        nbsp = UNICODE_NBSP

        for line in spec_lines:

            pkg.current_linenum += 1

            char = line.find(nbsp)
            if char != -1:
                self.output.add_info('W', pkg, 'non-break-space', 'line %s, char %d' %
                                     (pkg.current_linenum, char))

            section_marker = False
            for sec, regex in section_regexs.items():
                res = regex.search(line)
                if res:
                    current_section = sec
                    section_marker = True
                    section[sec] = section.get(sec, 0) + 1
                    if sec in ('package', 'files'):
                        rest = filelist_regex.sub('', line[res.end() - 1:])
                        res = pkgname_regex.search(rest)
                        if res:
                            current_package = res.group(1)
                        else:
                            current_package = None
                    break

            if section_marker:

                if not is_lib_pkg and lib_package_regex.search(line):
                    is_lib_pkg = True

                continue

            if (current_section in Pkg.RPM_SCRIPTLETS + ('prep', 'build') and
                    contains_buildroot(line)):
                self.output.add_info('W', pkg, 'rpm-buildroot-usage', '%' + current_section,
                                     line[:-1].strip())

            if make_check_regex.search(line) and current_section not in \
                    ('check', 'changelog', 'package', 'description'):
                self.output.add_info('W', pkg, 'make-check-outside-check-section',
                                     line[:-1])

            if current_section in buildroot_clean and \
                    not buildroot_clean[current_section] and \
                    contains_buildroot(line) and rm_regex.search(line):
                buildroot_clean[current_section] = True

            if ifarch_regex.search(line):
                if_depth = if_depth + 1
                ifarch_depth = if_depth

            if if_regex.search(line):
                if_depth = if_depth + 1

            if setup_regex.match(line):
                if not setup_q_regex.search(line):
                    # Don't warn if there's a -T without -a or -b
                    if setup_t_regex.search(line):
                        if setup_ab_regex.search(line):
                            self.output.add_info('W', pkg, 'setup-not-quiet')
                    else:
                        self.output.add_info('W', pkg, 'setup-not-quiet')
                if current_section != 'prep':
                    self.output.add_info('W', pkg, 'setup-not-in-prep')
            elif autopatch_regex.search(line):
                patches_auto_applied = True
                if current_section != 'prep':
                    self.output.add_info('W', pkg, '%autopatch-not-in-prep')
            else:
                res = autosetup_regex.search(line)
                if res:
                    if not autosetup_n_regex.search(res.group(1)):
                        patches_auto_applied = True
                    if current_section != 'prep':
                        self.output.add_info('W', pkg, '%autosetup-not-in-prep')

            if endif_regex.search(line):
                if ifarch_depth == if_depth:
                    ifarch_depth = -1
                if_depth = if_depth - 1

            res = applied_patch_regex.search(line)
            if res:
                pnum = res.group(1) or 0
                for tmp in applied_patch_p_regex.findall(line) or [pnum]:
                    pnum = int(tmp)
                    applied_patches.append(pnum)
                    if ifarch_depth > 0:
                        applied_patches_ifarch.append(pnum)
            else:
                res = applied_patch_pipe_regex.search(line)
                if res:
                    pnum = int(res.group(1))
                    applied_patches.append(pnum)
                    if ifarch_depth > 0:
                        applied_patches_ifarch.append(pnum)
                else:
                    res = applied_patch_i_regex.search(line)
                    if res:
                        pnum = int(res.group(1))
                        applied_patches.append(pnum)
                        if ifarch_depth > 0:
                            applied_patches_ifarch.append(pnum)
            if not res and not source_dir:
                res = source_dir_regex.search(line)
                if res:
                    source_dir = True
                    self.output.add_info('E', pkg, 'use-of-RPM_SOURCE_DIR')

            if configure_linenum:
                if configure_cmdline[-1] == '\\':
                    configure_cmdline = configure_cmdline[:-1] + line.strip()
                else:
                    res = configure_libdir_spec_regex.search(configure_cmdline)
                    if not res:
                        # Hack to get the correct (start of ./configure) line
                        # number displayed:
                        real_linenum = pkg.current_linenum
                        pkg.current_linenum = configure_linenum
                        self.output.add_info('W', pkg, 'configure-without-libdir-spec')
                        pkg.current_linenum = real_linenum
                    elif res.group(1):
                        res = re.match(hardcoded_library_paths, res.group(1))
                        if res:
                            self.output.add_info('E', pkg, 'hardcoded-library-path',
                                                 res.group(1), 'in configure options')
                    configure_linenum = None

            hashPos = line.find('#')

            if current_section != 'changelog':
                cfgPos = line.find('./configure')
                if cfgPos != -1 and (hashPos == -1 or hashPos > cfgPos):
                    # store line where it started
                    configure_linenum = pkg.current_linenum
                    configure_cmdline = line.strip()

            res = hardcoded_library_path_regex.search(line)
            if current_section != 'changelog' and res and not \
                    (biarch_package_regex.match(pkg.name) or
                     self.hardcoded_lib_path_exceptions_regex.search(
                         res.group(1).lstrip())):
                self.output.add_info('E', pkg, 'hardcoded-library-path', 'in',
                                     res.group(1).lstrip())

            if '%mklibname' in line:
                mklibname = True

            if current_section == 'package':

                # Would be cleaner to get sources and patches from the
                # specfile parsed in Python (see below), but we want to
                # catch %ifarch'd etc ones as well, and also catch these when
                # the specfile is not parseable.

                res = patch_regex.search(line)
                if res:
                    pnum = int(res.group(1) or 0)
                    patches[pnum] = res.group(2)

                res = obsolete_tags_regex.search(line)
                if res:
                    self.output.add_info('W', pkg, 'obsolete-tag', res.group(1))

                res = buildroot_regex.search(line)
                if res:
                    buildroot = True
                    if res.group(1).startswith('/'):
                        self.output.add_info('W', pkg, 'hardcoded-path-in-buildroot-tag',
                                             res.group(1))

                res = buildarch_regex.search(line)
                if res:
                    if res.group(1) != 'noarch':
                        self.output.add_info('E', pkg,
                                             'buildarch-instead-of-exclusivearch-tag',
                                             res.group(1))
                    else:
                        package_noarch[current_package] = True

                res = packager_regex.search(line)
                if res:
                    self.output.add_info('W', pkg, 'hardcoded-packager-tag', res.group(1))

                res = prefix_regex.search(line)
                if res:
                    if not res.group(1).startswith('%'):
                        self.output.add_info('W', pkg, 'hardcoded-prefix-tag', res.group(1))

                res = prereq_regex.search(line)
                if res:
                    self.output.add_info('E', pkg, 'prereq-use', res.group(2))

                res = buildprereq_regex.search(line)
                if res:
                    self.output.add_info('E', pkg, 'buildprereq-use', res.group(1))

                if scriptlet_requires_regex.search(line):
                    self.output.add_info('E', pkg, 'broken-syntax-in-scriptlet-requires',
                                         line.strip())

                res = requires_regex.search(line)
                if res:
                    reqs = Pkg.parse_deps(res.group(1))
                    e = Pkg.has_forbidden_controlchars(reqs)
                    if e:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             'Requires: %s' % e)
                    for req in unversioned(reqs):
                        if compop_regex.search(req):
                            self.output.add_info('W', pkg,
                                                 'comparison-operator-in-deptoken',
                                                 req)

                res = provides_regex.search(line)
                if res:
                    provs = Pkg.parse_deps(res.group(1))
                    e = Pkg.has_forbidden_controlchars(provs)
                    if e:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             'Provides: %s' % e)
                    for prov in unversioned(provs):
                        if not prov.startswith('/'):
                            self.output.add_info('W', pkg, 'unversioned-explicit-provides',
                                                 prov)
                        if compop_regex.search(prov):
                            self.output.add_info('W', pkg,
                                                 'comparison-operator-in-deptoken',
                                                 prov)

                res = obsoletes_regex.search(line)
                if res:
                    obses = Pkg.parse_deps(res.group(1))
                    e = Pkg.has_forbidden_controlchars(obses)
                    if e:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             'Obsoletes: %s' % e)
                    for obs in unversioned(obses):
                        if not obs.startswith('/'):
                            self.output.add_info('W', pkg, 'unversioned-explicit-obsoletes',
                                                 obs)
                        if compop_regex.search(obs):
                            self.output.add_info('W', pkg,
                                                 'comparison-operator-in-deptoken',
                                                 obs)

                res = conflicts_regex.search(line)
                if res:
                    confs = Pkg.parse_deps(res.group(1))
                    e = Pkg.has_forbidden_controlchars(confs)
                    if e:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             'Conflicts: %s' % e)
                    for conf in unversioned(confs):
                        if compop_regex.search(conf):
                            self.output.add_info('W', pkg,
                                                 'comparison-operator-in-deptoken',
                                                 conf)

            if current_section == 'changelog':
                e = Pkg.has_forbidden_controlchars(line)
                if e:
                    self.output.add_info('E', pkg,
                                         'forbidden-controlchar-found',
                                         '%%changelog: %s' % e)
                for match in self.macro_regex.findall(line):
                    res = re.match('%+', match)
                    if len(res.group(0)) % 2:
                        self.output.add_info('W', pkg, 'macro-in-%changelog', match)
            else:
                if not depscript_override:
                    depscript_override = \
                        depscript_override_regex.search(line) is not None
                if not depgen_disabled:
                    depgen_disabled = \
                        depgen_disable_regex.search(line) is not None
                if not patch_fuzz_override:
                    patch_fuzz_override = \
                        patch_fuzz_override_regex.search(line) is not None

            if current_section == 'files':
                # TODO: check scriptlets for these too?
                if package_noarch.get(current_package) or \
                        (current_package not in package_noarch and
                         package_noarch.get(None)):
                    res = libdir_regex.search(line)
                    if res:
                        pkgname = current_package
                        if pkgname is None:
                            pkgname = '(main package)'
                        self.output.add_info('W', pkg, 'libdir-macro-in-noarch-package',
                                             pkgname, line.rstrip())

            if not indent_tabs and '\t' in line:
                indent_tabs = pkg.current_linenum
            if not indent_spaces and indent_spaces_regex.search(line):
                indent_spaces = pkg.current_linenum

            # Check if egrep or fgrep is used
            if current_section not in \
                    ('package', 'changelog', 'description', 'files'):
                greps = deprecated_grep_regex.findall(line)
                if greps:
                    self.output.add_info('W', pkg, 'deprecated-grep', greps)

            # If not checking spec file only, we're checking one inside a
            # SRPM -> skip this check to avoid duplicate warnings (#167)
            if spec_only and self.valid_groups and \
               line.lower().startswith('group:'):
                group = line[6:].strip()
                if group not in self.valid_groups:
                    self.output.add_info('W', pkg, 'non-standard-group', group)

            # Test if there are macros in comments
            if hashPos != -1 and \
                    (hashPos == 0 or line[hashPos - 1] in (' ', '\t')):
                for match in self.macro_regex.findall(
                        line[hashPos + 1:]):
                    res = re.match('%+', match)
                    if len(res.group(0)) % 2:
                        self.output.add_info('W', pkg, 'macro-in-comment', match)

        # Last line read is not useful after this point
        pkg.current_linenum = None

        for sect in (x for x in buildroot_clean if not buildroot_clean[x]):
            self.output.add_info('W', pkg, 'no-cleaning-of-buildroot', '%' + sect)

        if not buildroot:
            self.output.add_info('W', pkg, 'no-buildroot-tag')

        for sec in ('prep', 'build', 'install', 'clean'):
            if not section.get(sec):
                self.output.add_info('W', pkg, 'no-%%%s-section' % sec)
        for sec in ('changelog',):
            # prep, build, install, clean, check prevented by rpmbuild 4.4
            if section.get(sec, 0) > 1:
                self.output.add_info('W', pkg, 'more-than-one-%%%s-section' % sec)

        if is_lib_pkg and not mklibname:
            self.output.add_info('E', pkg, 'lib-package-without-%mklibname')

        if depscript_override and not depgen_disabled:
            self.output.add_info('W', pkg, 'depscript-without-disabling-depgen')

        if patch_fuzz_override:
            self.output.add_info('W', pkg, 'patch-fuzz-is-changed')

        if indent_spaces and indent_tabs:
            pkg.current_linenum = max(indent_spaces, indent_tabs)
            self.output.add_info('W', pkg, 'mixed-use-of-spaces-and-tabs',
                                 '(spaces: line %d, tab: line %d)' %
                                 (indent_spaces, indent_tabs))
            pkg.current_linenum = None

        # process gathered info
        if not patches_auto_applied:
            for pnum, pfile in patches.items():
                if pnum in applied_patches_ifarch:
                    self.output.add_info('W', pkg, '%ifarch-applied-patch',
                                         'Patch%d:' % pnum, pfile)
                if pnum not in applied_patches:
                    self.output.add_info('W', pkg, 'patch-not-applied',
                                         'Patch%d:' % pnum, pfile)

        # Rest of the checks require a real spec file
        if not self._spec_file:
            return

        # We'd like to parse the specfile only once using python bindings,
        # but it seems errors from rpmlib get logged to stderr and we can't
        # capture and print them nicely, so we do it once each way :P

        outcmd = subprocess.run(
            ('rpm', '-q', '--qf=', '-D', '_sourcedir %s' % Path(self._spec_file).parent, '--specfile', self._spec_file), stdout=subprocess.PIPE)
        text = outcmd.stdout.decode()
        if text.endswith('\n'):
            text = text[:-1]

        parse_error = False
        for line in text.splitlines():
            # No such file or dir hack: https://bugzilla.redhat.com/487855
            if 'No such file or directory' not in line:
                parse_error = True
                self.output.add_info('E', pkg, 'specfile-error', line)

        if not parse_error:
            # grab sources and patches from parsed spec object to get
            # them with macros expanded for URL checking

            spec_obj = None
            rpm.addMacro('_sourcedir', pkg.dirName())
            try:
                ts = rpm.TransactionSet()
                spec_obj = ts.parseSpec(str(self._spec_file))
            except (ValueError, rpm.error):
                # errors logged above already
                pass
            rpm.delMacro('_sourcedir')
            if spec_obj:
                try:
                    # rpm < 4.8.0
                    sources = spec_obj.sources()
                except TypeError:
                    # rpm >= 4.8.0
                    sources = spec_obj.sources
                for src in sources:
                    (url, num, flags) = src
                    (scheme, netloc) = urlparse(url)[0:2]
                    if flags & 1:  # rpmspec.h, rpm.org ticket #123
                        srctype = 'Source'
                    else:
                        srctype = 'Patch'
                    tag = '%s%s' % (srctype, num)
                    if scheme and netloc:
                        continue
                    elif srctype == 'Source' and tarball_regex.search(url):
                        self.output.add_info('W', pkg, 'invalid-url', '%s:' % tag, url)
Example #4
0
    def check_spec(self, pkg):
        """Find specfile in specified path and run spec file related checks."""
        self._spec_file = pkg.name
        spec_only = isinstance(pkg, Pkg.FakePkg)
        spec_lines = readlines(self._spec_file)
        patches = {}
        applied_patches = []
        applied_patches_ifarch = []
        patches_auto_applied = False
        source_dir = False
        buildroot = False
        configure_linenum = None
        configure_cmdline = ''
        mklibname = False
        is_lib_pkg = False
        if_depth = 0
        ifarch_depth = -1
        current_section = 'package'
        buildroot_clean = {'clean': False, 'install': False}
        depscript_override = False
        depgen_disabled = False
        patch_fuzz_override = False
        indent_spaces = 0
        indent_tabs = 0
        section = {}
        # None == main package
        current_package = None
        package_noarch = {}

        # method call
        self._check_non_utf8_spec_file(pkg)

        # gather info from spec lines

        pkg.current_linenum = 0

        nbsp = UNICODE_NBSP

        # Analyse specfile line by line to check for (E)rrors or (W)arnings
        for line in spec_lines:

            pkg.current_linenum += 1

            char = line.find(nbsp)
            if char != -1:
                self.output.add_info(
                    'W', pkg, 'non-break-space',
                    'line %s, char %d' % (pkg.current_linenum, char))

            section_marker = False
            for sec, regex in section_regexs.items():
                res = regex.search(line)
                if res:
                    current_section = sec
                    section_marker = True
                    section[sec] = section.get(sec, 0) + 1
                    if sec in ('package', 'files'):
                        rest = filelist_regex.sub('', line[res.end() - 1:])
                        res = pkgname_regex.search(rest)
                        if res:
                            current_package = res.group(1)
                        else:
                            current_package = None
                    break

            if section_marker:

                if not is_lib_pkg and lib_package_regex.search(line):
                    is_lib_pkg = True

                continue

            if (current_section in Pkg.RPM_SCRIPTLETS + ('prep', 'build')
                    and contains_buildroot(line)):
                self.output.add_info('W', pkg, 'rpm-buildroot-usage',
                                     '%' + current_section, line[:-1].strip())

            if make_check_regex.search(line) and current_section not in \
                    ('check', 'changelog', 'package', 'description'):
                self.output.add_info('W', pkg,
                                     'make-check-outside-check-section',
                                     line[:-1])

            if current_section in buildroot_clean and \
                    not buildroot_clean[current_section] and \
                    contains_buildroot(line) and rm_regex.search(line):
                buildroot_clean[current_section] = True

            if ifarch_regex.search(line):
                if_depth = if_depth + 1
                ifarch_depth = if_depth

            if if_regex.search(line):
                if_depth = if_depth + 1

            if setup_regex.match(line):
                if not setup_q_regex.search(line):
                    # Don't warn if there's a -T without -a or -b
                    if setup_t_regex.search(line):
                        if setup_ab_regex.search(line):
                            self.output.add_info('W', pkg, 'setup-not-quiet')
                    else:
                        self.output.add_info('W', pkg, 'setup-not-quiet')
                if current_section != 'prep':
                    self.output.add_info('W', pkg, 'setup-not-in-prep')
            elif autopatch_regex.search(line):
                patches_auto_applied = True
                if current_section != 'prep':
                    self.output.add_info('W', pkg, '%autopatch-not-in-prep')
            else:
                res = autosetup_regex.search(line)
                if res:
                    if not autosetup_n_regex.search(res.group(1)):
                        patches_auto_applied = True
                    if current_section != 'prep':
                        self.output.add_info('W', pkg,
                                             '%autosetup-not-in-prep')

            if endif_regex.search(line):
                if ifarch_depth == if_depth:
                    ifarch_depth = -1
                if_depth = if_depth - 1

            res = applied_patch_regex.search(line)
            if res:
                pnum = res.group(1) or 0
                for tmp in applied_patch_p_regex.findall(line) or [pnum]:
                    pnum = int(tmp)
                    applied_patches.append(pnum)
                    if ifarch_depth > 0:
                        applied_patches_ifarch.append(pnum)
            else:
                res = applied_patch_pipe_regex.search(line)
                if res:
                    pnum = int(res.group(1))
                    applied_patches.append(pnum)
                    if ifarch_depth > 0:
                        applied_patches_ifarch.append(pnum)
                else:
                    res = applied_patch_i_regex.search(line)
                    if res:
                        pnum = int(res.group(1))
                        applied_patches.append(pnum)
                        if ifarch_depth > 0:
                            applied_patches_ifarch.append(pnum)
            if not res and not source_dir:
                res = source_dir_regex.search(line)
                if res:
                    source_dir = True
                    self.output.add_info('E', pkg, 'use-of-RPM_SOURCE_DIR')

            if configure_linenum:
                if configure_cmdline[-1] == '\\':
                    configure_cmdline = configure_cmdline[:-1] + line.strip()
                else:
                    res = configure_libdir_spec_regex.search(configure_cmdline)
                    if not res:
                        # Hack to get the correct (start of ./configure) line
                        # number displayed:
                        real_linenum = pkg.current_linenum
                        pkg.current_linenum = configure_linenum
                        self.output.add_info('W', pkg,
                                             'configure-without-libdir-spec')
                        pkg.current_linenum = real_linenum
                    elif res.group(1):
                        res = re.match(hardcoded_library_paths, res.group(1))
                        if res:
                            self.output.add_info('E', pkg,
                                                 'hardcoded-library-path',
                                                 res.group(1),
                                                 'in configure options')
                    configure_linenum = None

            hash_pos = line.find('#')

            if current_section != 'changelog':
                cfg_pos = line.find('./configure')
                if cfg_pos != -1 and (hash_pos == -1 or hash_pos > cfg_pos):
                    # store line where it started
                    configure_linenum = pkg.current_linenum
                    configure_cmdline = line.strip()

            res = hardcoded_library_path_regex.search(line)
            if current_section != 'changelog' and res and not \
                    (biarch_package_regex.match(pkg.name) or
                     self.hardcoded_lib_path_exceptions_regex.search(
                         res.group(1).lstrip())):
                self.output.add_info('E', pkg, 'hardcoded-library-path', 'in',
                                     res.group(1).lstrip())

            if '%mklibname' in line:
                mklibname = True

            if current_section == 'package':

                # Would be cleaner to get sources and patches from the
                # specfile parsed in Python (see below), but we want to
                # catch %ifarch'd etc ones as well, and also catch these when
                # the specfile is not parseable.

                res = patch_regex.search(line)
                if res:
                    pnum = int(res.group(1) or 0)
                    patches[pnum] = res.group(2)

                res = obsolete_tags_regex.search(line)
                if res:
                    self.output.add_info('W', pkg, 'obsolete-tag',
                                         res.group(1))

                res = buildroot_regex.search(line)
                if res:
                    buildroot = True
                    if res.group(1).startswith('/'):
                        self.output.add_info(
                            'W', pkg, 'hardcoded-path-in-buildroot-tag',
                            res.group(1))

                res = buildarch_regex.search(line)
                if res:
                    if res.group(1) != 'noarch':
                        self.output.add_info(
                            'E', pkg, 'buildarch-instead-of-exclusivearch-tag',
                            res.group(1))
                    else:
                        package_noarch[current_package] = True

                res = packager_regex.search(line)
                if res:
                    self.output.add_info('W', pkg, 'hardcoded-packager-tag',
                                         res.group(1))

                res = prefix_regex.search(line)
                if res:
                    if not res.group(1).startswith('%'):
                        self.output.add_info('W', pkg, 'hardcoded-prefix-tag',
                                             res.group(1))

                res = prereq_regex.search(line)
                if res:
                    self.output.add_info('E', pkg, 'prereq-use', res.group(2))

                res = buildprereq_regex.search(line)
                if res:
                    self.output.add_info('E', pkg, 'buildprereq-use',
                                         res.group(1))

                res = requires_regex.search(line)
                if res:
                    reqs = Pkg.parse_deps(res.group(1))
                    deptoken = Pkg.has_forbidden_controlchars(reqs)
                    if deptoken:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             f'Requires: {deptoken}')
                    for req in unversioned(reqs):
                        if compop_regex.search(req):
                            self.output.add_info(
                                'W', pkg, 'comparison-operator-in-deptoken',
                                req)

                res = provides_regex.search(line)
                if res:
                    provs = Pkg.parse_deps(res.group(1))
                    deptoken = Pkg.has_forbidden_controlchars(provs)
                    if deptoken:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             f'Provides: {deptoken}')
                    for prov in unversioned(provs):
                        if not prov.startswith('/'):
                            self.output.add_info(
                                'W', pkg, 'unversioned-explicit-provides',
                                prov)
                        if compop_regex.search(prov):
                            self.output.add_info(
                                'W', pkg, 'comparison-operator-in-deptoken',
                                prov)

                res = obsoletes_regex.search(line)
                if res:
                    obses = Pkg.parse_deps(res.group(1))
                    deptoken = Pkg.has_forbidden_controlchars(obses)
                    if deptoken:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             f'Obsoletes: {deptoken}')
                    for obs in unversioned(obses):
                        if not obs.startswith('/'):
                            self.output.add_info(
                                'W', pkg, 'unversioned-explicit-obsoletes',
                                obs)
                        if compop_regex.search(obs):
                            self.output.add_info(
                                'W', pkg, 'comparison-operator-in-deptoken',
                                obs)

                res = conflicts_regex.search(line)
                if res:
                    confs = Pkg.parse_deps(res.group(1))
                    deptoken = Pkg.has_forbidden_controlchars(confs)
                    if deptoken:
                        self.output.add_info('E', pkg,
                                             'forbidden-controlchar-found',
                                             f'Conflicts: {deptoken}')
                    for conf in unversioned(confs):
                        if compop_regex.search(conf):
                            self.output.add_info(
                                'W', pkg, 'comparison-operator-in-deptoken',
                                conf)

            if current_section == 'changelog':
                deptoken = Pkg.has_forbidden_controlchars(line)
                if deptoken:
                    self.output.add_info('E', pkg,
                                         'forbidden-controlchar-found',
                                         '%%changelog: %s' % deptoken)
                for match in self.macro_regex.findall(line):
                    res = re.match('%+', match)
                    if len(res.group(0)) % 2:
                        self.output.add_info('W', pkg, 'macro-in-%changelog',
                                             match)
            else:
                if not depscript_override:
                    depscript_override = \
                        depscript_override_regex.search(line) is not None
                if not depgen_disabled:
                    depgen_disabled = \
                        depgen_disable_regex.search(line) is not None
                if not patch_fuzz_override:
                    patch_fuzz_override = \
                        patch_fuzz_override_regex.search(line) is not None

            if current_section == 'files':
                # TODO: check scriptlets for these too?
                if package_noarch.get(current_package) or \
                        (current_package not in package_noarch and
                         package_noarch.get(None)):
                    res = libdir_regex.search(line)
                    if res:
                        pkgname = current_package
                        if pkgname is None:
                            pkgname = '(main package)'
                        self.output.add_info('W', pkg,
                                             'libdir-macro-in-noarch-package',
                                             pkgname, line.rstrip())

            if not indent_tabs and '\t' in line:
                indent_tabs = pkg.current_linenum
            if not indent_spaces and indent_spaces_regex.search(line):
                indent_spaces = pkg.current_linenum

            # Check if egrep or fgrep is used
            if current_section not in \
                    ('package', 'changelog', 'description', 'files'):
                greps = deprecated_grep_regex.findall(line)
                if greps:
                    self.output.add_info('W', pkg, 'deprecated-grep', greps)

            # If not checking spec file only, we're checking one inside a
            # SRPM -> skip this check to avoid duplicate warnings (#167)
            if spec_only and self.valid_groups and \
               line.lower().startswith('group:'):
                group = line[6:].strip()
                if group not in self.valid_groups:
                    self.output.add_info('W', pkg, 'non-standard-group', group)

            # Test if there are macros in comments
            if hash_pos != -1 and \
                    (hash_pos == 0 or line[hash_pos - 1] in (' ', '\t')):
                for match in self.macro_regex.findall(line[hash_pos + 1:]):
                    res = re.match('%+', match)
                    if len(res.group(0)) % 2:
                        self.output.add_info('W', pkg, 'macro-in-comment',
                                             match)

        # Last line read is not useful after this point
        pkg.current_linenum = None

        # Run checks for whole package
        self._check_no_cleaning_of_buildroot(pkg, buildroot_clean)
        self._check_no_buildroot_tag(pkg, buildroot)
        self._check_no_s_section(pkg, section)
        self._check_more_than_one_changelog_section(pkg, section)
        self._check_lib_package_without_mklibname(pkg, is_lib_pkg, mklibname)
        self._check_descript_without_disabling_depgen(pkg, depscript_override,
                                                      depgen_disabled)
        self._check_patch_fuzz_is_changed(pkg, patch_fuzz_override)
        self._check_mixed_use_of_space_and_tabs(pkg, indent_spaces,
                                                indent_tabs)
        self.check_ifarch_and_not_applied_patches(pkg, patches_auto_applied,
                                                  patches,
                                                  applied_patches_ifarch,
                                                  applied_patches)
        # Checks below require a real spec file
        if not self._spec_file:
            return
        self._check_specfile_error(pkg, subprocess)
        self._check_invalid_url(pkg, rpm)
Example #5
0
    def _check_changelog_tag(self, pkg, changelog, version, release, name,
                             epoch):
        """Trigger multiple check of type *-changelog, *-changelogname-*, changelog-*
        and forbidden-controlchar

        Contains all the checks that cause an issue during build of the rpm
        in the %changelog of the specfile

        Args:
            changelog: Find the %changelog in the specfile

        Returns:
            Output info to STDOUT
        """

        # Check if a package does not have a %changelog in its spec file
        if not changelog:
            self.output.add_info('E', pkg, 'no-changelogname-tag')
        else:
            clt = pkg[rpm.RPMTAG_CHANGELOGTEXT]
            if self.use_version_in_changelog:
                ret = changelog_version_regex.search(
                    byte_to_string(changelog[0]))
                if not ret and clt:
                    # we also allow the version specified as the first
                    # thing on the first line of the text
                    ret = changelog_text_version_regex.search(
                        byte_to_string(clt[0]))
                # Check if a package does not have version in the %changelog in latest version
                if not ret:
                    self.output.add_info('W', pkg,
                                         'no-version-in-last-changelog')
                elif version and release:
                    srpm = pkg[rpm.RPMTAG_SOURCERPM] or ''
                    # only check when source name correspond to name
                    if srpm[0:-8] == '%s-%s-%s' % (name, version, release):
                        expected = [version + '-' + release]
                        if epoch is not None:  # regardless of use_epoch
                            expected[0] = str(epoch) + ':' + expected[0]
                        # Allow EVR in changelog without release extension,
                        # the extension is often a macro or otherwise dynamic.
                        if self.release_ext:
                            expected.append(
                                self.extension_regex.sub('', expected[0]))
                        # Check if a package does not have a version that is
                        # compatible with epoch:vesrion-release tuple
                        if ret.group(1) not in expected:
                            if len(expected) == 1:
                                expected = expected[0]
                            self.output.add_info(
                                'W', pkg, 'incoherent-version-in-changelog',
                                ret.group(1), expected)
            if clt:
                changelog = changelog + clt
            for deptoken in changelog:
                dep = Pkg.has_forbidden_controlchars(deptoken)
                # Check if a package contains a forbidden character in %changelog
                if dep:
                    self.output.add_info('E', pkg,
                                         'forbidden-controlchar-found',
                                         '%%changelog : %s' % dep)
                    break

            clt = pkg[rpm.RPMTAG_CHANGELOGTIME][0]
            if clt:
                clt -= clt % (24 * 3600)  # roll back to 00:00:00, see #246
                # Check if a package contains a changelog entry that is suspiciously too far behind
                if clt < oldest_changelog_timestamp:
                    self.output.add_info(
                        'W', pkg, 'changelog-time-overflow',
                        time.strftime('%Y-%m-%d', time.gmtime(clt)))
                # Check if a package contians a entry in %changelog
                # with timestamp thats in the future of its writing
                elif clt > time.time():
                    self.output.add_info(
                        'E', pkg, 'changelog-time-in-future',
                        time.strftime('%Y-%m-%d', time.gmtime(clt)))